Skip to content

02 Get your data in /Integrations

Featurebase

Send Featurebase posts and comments to Dimension Labs with the universal API.

  1. F FeaturebaseTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. 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.

  • 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. In your Featurebase dashboard, open Settings > API.
  2. Create a key and copy it. It starts with sk_.
  3. Send it as a Bearer token on every request, with a pinned Featurebase-Version header (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.

List posts newest first:

Terminal window
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"
  • limit is 1 to 100. Pass the response’s nextCursor as cursor until it is null.
  • There is no “created since” filter: read newest first and stop once you reach posts older than your cursor.
  • kind defaults to issue (requests). In workspaces where Featurebase turns customer submissions into insights, those submissions are kind=record. The code reads both.
  • content is 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.

Dimension LabsFeaturebaseNotes
textpost title and content; comment contentBoth are HTML: the code strips it. Skip comments where isPrivate, isDeleted or isSpam is true; private comments are visible only to admins.
userIdauthor.idFeaturebase’s internal id. On your team’s replies, and when author.id is null, fall back to the sessionId.
incomingauthor.typecustomer, guest and lead are incoming: true. admin, integration and bot are your side.
sessionIdpost id (on a comment: postId)Prefix it, for example featurebase-abc123. The code skips changelog comments, which have no postId.
conversationIdpost idSame value as sessionId.
dimensionlabs_timestampcreatedAtISO 8601. Convert with toMillis.
platformJsonpost kind, boardId, status.name, tags, upvotes; comment id, parentCommentId, upvotes, author.typeboardId 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.

// 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 side
const stripHtml = (html) => String(html ?? '').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/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;
}

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-01 sends 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.

Checked against Featurebase’s documentation on 2026-10-26.