02 Get your data in /Integrations
Productboard
Send Productboard feedback notes and conversations to Dimension Labs with the universal API.
- P ProductboardTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in each Productboard note as one conversation. A text note (feedback captured from a portal, email, the Chrome extension or by your team) arrives as one customer message. A conversation note keeps every part: the customer’s turns and your agents’ replies. You run it on your side: a small script pulls from the Productboard API and posts each message to the universal API.
What you need
Section titled “What you need”- A Productboard workspace on the Pro plan or higher. Productboard’s API tokens aren’t available on the Essentials plan.
- A Productboard user who can open the workspace’s integration settings and create an API token.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Productboard
Section titled “1. Get API access in Productboard”- Sign in to your Productboard workspace.
- Go to Settings > Integrations > Public APIs > Access Token. If you don’t see it, the workspace is probably on the Essentials plan.
- Click the plus (+) icon to create a token, and copy it. Productboard shows it only once.
- Send it as a Bearer token, with
Accept: application/json, on every request (API token authentication).
Treat the token like a password, and rotate it from the same settings page.
2. Pull new notes
Section titled “2. Pull new notes”List notes with updatedFrom set to your cursor:
curl -g -H "Authorization: Bearer $PRODUCTBOARD_TOKEN" -H "Accept: application/json" \ "https://api.productboard.com/v2/notes?updatedFrom=2026-10-01T00:00:00Z&type[]=textNote&type[]=conversationNote"updatedFromandupdatedTotake ISO 8601 date-times and are inclusive.createdFromandcreatedTowork the same way.type[]limits the list totextNote,conversationNoteoropportunityNote. The code reads text and conversation notes.- Results come newest first. Follow
links.next(a full URL) until it isnull. - Owner and creator email addresses come back redacted unless the token has the
members:pii:readscope. You don’t need them.
Rate limits, per Productboard: 50 requests per second per token. Going over returns 429 with a Retry-After header (introduction, rate limits).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Productboard | Notes |
|---|---|---|
text | text note: fields.name and fields.content; conversation note: each part’s content | content is rich text that can hold HTML: the code strips it. Parts with no text are skipped. |
userId | the note’s customer relationship, when its target is a user | Productboard’s internal user id. When the customer is a company, or there is none, fall back to the sessionId. |
incoming | text note: always true; conversation part: authorType | authorType is customer or agent. Only customer is incoming: true. |
sessionId | note id | Prefix it, for example productboard-123e4567-.... |
conversationId | note id | Same value as sessionId. |
dimensionlabs_timestamp | text note: createdAt; conversation part: timestamp | Convert with toMillis. Parts with the same time are spaced 1,000 ms apart so they keep their order. |
platformJson | type, fields.name, fields.tags, the company id when the customer is a company, metadata.source.system | The source system tells you which integration captured the note. |
Personal data. Don’t send owner, creator or customer email addresses, or authorName from conversation parts. Notes often quote the customer’s name, email signature or account details: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// productboard.mjs. Environment: PRODUCTBOARD_TOKEN, DL_API_KEY.import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const headers = { Authorization: `Bearer ${process.env.PRODUCTBOARD_TOKEN}`, Accept: 'application/json' };const stripHtml = (html) => String(html ?? '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>') .replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&').replace(/\s+/g, ' ').trim();
// Retries Productboard's rate limit (429) and server errors (5xx), honoring Retry-After.async function get(url) { for (let attempt = 0; ; attempt++) { const res = await fetch(url, { headers }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Productboard returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Turns one note into messages: a text note is one incoming message, a conversation note one message per part.function toMessages(note) { const f = note.fields ?? {}; if (note.type === 'conversationNote') { let last = 0; return (Array.isArray(f.content) ? f.content : []).map((part) => { let ts = toMillis(part.timestamp); if (ts <= last) ts = last + 1000; // parts with the same time keep their order last = ts; return { text: stripHtml(part.content), incoming: part.authorType === 'customer', ts, part_id: part.externalId }; }); } const text = [f.name, stripHtml(f.content)].filter(Boolean).join('\n\n'); return [{ text, incoming: true, ts: toMillis(note.createdAt ?? f.createdAt) }];}
// Sends every text and conversation note updated at or after `since` (Unix ms). Returns the new `since`.export async function syncProductboard({ since, historical = false }) { let newest = since; const params = new URLSearchParams({ updatedFrom: new Date(since).toISOString() }); params.append('type[]', 'textNote'); params.append('type[]', 'conversationNote'); for (let url = `https://api.productboard.com/v2/notes?${params}`; url; ) { const page = await get(url); for (const note of page.data) { const rels = Array.isArray(note.relationships) ? note.relationships : note.relationships?.data ?? []; const customer = rels.find((r) => r.type === 'customer')?.target; const sessionId = `productboard-${note.id}`; for (const m of toMessages(note)) { if (!m.text) continue; await sendToDimensionLabs({ text: m.text, userId: customer?.type === 'user' ? customer.id : sessionId, incoming: m.incoming, sessionId, conversationId: sessionId, dimensionlabs_timestamp: m.ts, platformJson: { note_type: note.type, title: note.fields?.name, tags: note.fields?.tags?.map((t) => t.name), company_id: customer?.type === 'company' ? customer.id : undefined, source_system: note.metadata?.source?.system, part_id: m.part_id, }, }, { historical: historical || m.ts < Date.now() - HOUR }); } newest = Math.max(newest, toMillis(note.updatedAt ?? note.fields?.updatedAt)); } url = page.links?.next ?? null; } return newest;}5. Run it
Section titled “5. Run it”Save the code as productboard.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncProductboard as sync } from './productboard.mjs';.
- History first:
node run.mjs 2025-01-01sends every note updated since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new and updated notes to
/track, and they are enriched automatically every day. A message that is more than an hour old when the job sees it goes to/trackhistorical, and needs a backfill. - Re-runs are safe: a note that changes (a new tag, a link to a feature) is sent again, and the duplicates are removed automatically.
One call returns a page of notes, so even a long history takes few requests; the code waits whenever Productboard asks it to.
No API access? If your workspace isn’t on the Pro plan, export your notes instead. Signed in to Productboard as a maker, open https://YOUR_WORKSPACE.productboard.com/api/notes/export in your browser. Productboard emails you a link to a CSV with id, created_at, note_title and note_text, among other columns (exporting your feedback into a CSV). Notes that came in through Zendesk, Slack, Intercom and other third-party integrations arrive with an empty note_text. Map the columns with the converter, check the file with the file checker, then upload it and run a backfill. Leave out the email and name columns.
Productboard reference
Section titled “Productboard reference”- Introduction to the Productboard API
- Authentication
- API token authentication
- List notes
- Rate limits
- Exporting your feedback into a CSV
Checked against Productboard’s documentation on 2026-10-26.