02 Get your data in /Integrations
Featurebase
Send Featurebase posts and comments to Dimension Labs with the universal API.
- F FeaturebaseTickets, 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 Featurebase post as one conversation: the feedback post or feature request (title and content) and the public comments on it, from your users and from your team. You run it on your side: a small script pulls from the Featurebase API and posts each message to the universal API.
What you need
Section titled “What you need”- A Featurebase admin who can open Settings > API and create an API key.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Featurebase
Section titled “1. Get API access in Featurebase”- In your Featurebase dashboard, open Settings > API.
- Create a key and copy it. It starts with
sk_. - Send it as a Bearer token on every request, with a pinned
Featurebase-Versionheader (getting started with the Featurebase API).
An API key has full access to your workspace: keep it on your server. Pin the version (the code uses 2026-08-19.orbit): without the header, your workspace’s default applies, and someone can change that default in the dashboard.
2. Pull new posts and comments
Section titled “2. Pull new posts and comments”List posts newest first:
curl -H "Authorization: Bearer $FEATUREBASE_API_KEY" -H "Featurebase-Version: 2026-08-19.orbit" \ "https://do.featurebase.app/v2/posts?sortBy=createdAt&sortOrder=desc&limit=100"limitis 1 to 100. Pass the response’snextCursorascursoruntil it isnull.- There is no “created since” filter: read newest first and stop once you reach posts older than your cursor.
kinddefaults toissue(requests). In workspaces where Featurebase turns customer submissions into insights, those submissions arekind=record. The code reads both.contentis HTML.
List comments with GET /v2/comments?sortBy=new&privacy=public. Without postId, it returns comments across the whole organization, newest first. Each comment carries its postId (or a changelogId for comments on your changelog).
Featurebase doesn’t publish its rate limits. Going over returns 429 (rate_limit_error); Featurebase asks you to retry only 429 and 5xx responses, with exponential backoff.
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Featurebase | Notes |
|---|---|---|
text | post title and content; comment content | Both are HTML: the code strips it. Skip comments where isPrivate, isDeleted or isSpam is true; private comments are visible only to admins. |
userId | author.id | Featurebase’s internal id. On your team’s replies, and when author.id is null, fall back to the sessionId. |
incoming | author.type | customer, guest and lead are incoming: true. admin, integration and bot are your side. |
sessionId | post id (on a comment: postId) | Prefix it, for example featurebase-abc123. The code skips changelog comments, which have no postId. |
conversationId | post id | Same value as sessionId. |
dimensionlabs_timestamp | createdAt | ISO 8601. Convert with toMillis. |
platformJson | post kind, boardId, status.name, tags, upvotes; comment id, parentCommentId, upvotes, author.type | boardId tells feedback boards apart. |
Personal data. Don’t send author.email, author.name or author.profilePicture. Posts and comments can hold names, emails and account details: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// featurebase.mjs. Environment: FEATUREBASE_API_KEY, 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.FEATUREBASE_API_KEY}`, 'Featurebase-Version': '2026-08-19.orbit' };const CUSTOMER = new Set(['customer', 'guest', 'lead']); // admin, integration and bot are your sideconst stripHtml = (html) => String(html ?? '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>') .replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&').replace(/\s+/g, ' ').trim();
// Retries Featurebase's rate limits (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(`Featurebase returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Reads a newest-first list until it reaches items created before `since`.async function newSince(path, since) { const items = []; for (let cursor = null, first = true; first || cursor; first = false) { const page = await get(`https://do.featurebase.app/v2/${path}&limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`); const fresh = page.data.filter((x) => toMillis(x.createdAt) >= since); items.push(...fresh); cursor = fresh.length < page.data.length ? null : page.nextCursor; } return items;}
// Sends every post and public comment created at or after `since` (Unix ms). Returns the new `since`.export async function syncFeaturebase({ since, historical = false }) { let newest = since; const send = async (msg) => { if (!msg.text) return; const ts = msg.dimensionlabs_timestamp; await sendToDimensionLabs(msg, { historical: historical || ts < Date.now() - HOUR }); newest = Math.max(newest, ts); }; // Requests (kind=issue) and customer submissions filed as insights (kind=record). for (const kind of ['issue', 'record']) { for (const p of await newSince(`posts?kind=${kind}&sortBy=createdAt&sortOrder=desc`, since)) { const sessionId = `featurebase-${p.id}`; await send({ text: [p.title, stripHtml(p.content)].filter(Boolean).join('\n\n'), userId: p.author?.id ?? sessionId, incoming: CUSTOMER.has(p.author?.type), sessionId, conversationId: sessionId, dimensionlabs_timestamp: toMillis(p.createdAt), platformJson: { kind: p.kind, board_id: p.boardId, status: p.status?.name, tags: p.tags?.map((t) => t.name), upvotes: p.upvotes }, }); } } for (const c of await newSince('comments?sortBy=new&privacy=public', since)) { if (!c.postId || c.isPrivate || c.isDeleted || c.isSpam) continue; // changelog comments have no postId const sessionId = `featurebase-${c.postId}`; const incoming = CUSTOMER.has(c.author?.type); await send({ text: stripHtml(c.content), userId: (incoming && c.author?.id) || sessionId, incoming, sessionId, conversationId: sessionId, dimensionlabs_timestamp: toMillis(c.createdAt), platformJson: { comment_id: c.id, parent_id: c.parentCommentId, upvotes: c.upvotes, author_type: c.author?.type }, }); } return newest;}5. Run it
Section titled “5. Run it”Save the code as featurebase.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncFeaturebase as sync } from './featurebase.mjs';.
- History first:
node run.mjs 2025-01-01sends every post and comment since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new posts and comments 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: duplicates are removed automatically.
Posts and comments come from organization-wide lists, so a run costs a few calls, not one per post; the code waits whenever Featurebase asks it to. You can also upload a file of exported posts (check it with the file checker), then run a backfill.
Featurebase reference
Section titled “Featurebase reference”Checked against Featurebase’s documentation on 2026-10-26.