Skip to content

02 Get your data in /Integrations

HubSpot help desk and inbox

Send HubSpot help desk and conversations inbox threads, with their ticket ids, to Dimension Labs with the universal API.

  1. HH HubSpotTickets, 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 the message threads from HubSpot’s conversations inbox and help desk workspace (chat, email and other connected channels), one conversation per thread, with the associated ticket id so you can join to ticket data. You run it on your side: a small script pulls from the HubSpot Conversations API and posts each message to the universal API.

  • A HubSpot super admin, to create a private app. HubSpot requires super admin access for private apps.
  • The private app’s access token, with the conversations.read scope.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

HubSpot calls private apps “legacy apps”: still supported, without the newest app features (private apps).

  1. In HubSpot, go to Development, then Legacy apps in the left sidebar.
  2. In the top right, click Create legacy app and select Private.
  3. Name the app, then on the Scopes tab add conversations.read.
  4. Click Create app.
  5. Open the app, go to the Auth tab, click Show token and copy it.
  6. Send it as Authorization: Bearer YOUR_TOKEN on every call.

Retrieve threads sorted by their latest message, starting from your cursor. association=TICKET adds the ticket id to threads that have one.

Terminal window
curl -H "Authorization: Bearer $HUBSPOT_TOKEN" \
"https://api.hubspot.com/conversations/v3/conversations/threads?sort=latestMessageTimestamp&latestMessageTimestampAfter=2026-10-13T00:00:00Z&limit=500&association=TICKET"

sort=latestMessageTimestamp needs latestMessageTimestampAfter. Results are always in ascending order, and limit goes up to 500. To get the next set, repeat the call with latestMessageTimestampAfter set to the last thread’s latestMessageTimestamp.

For each thread, GET /conversations/v3/conversations/threads/{threadId}/messages returns its message history. Page through it with the after parameter, using paging.next.after from the response. The history also holds internal comments and system entries: keep only type MESSAGE.

HubSpot’s semantic-version APIs such as /conversations/v3/ remain supported alongside its newer date-based versions (API versioning).

Rate limits, per HubSpot, for a private app: 100 requests per 10 seconds on Free and Starter, 190 on Professional and Enterprise. Daily limits per account are 250,000 on Free and Starter, 625,000 on Professional, and 1,000,000 on Enterprise (usage guidelines).

Dimension LabsHubSpotNotes
textmessage textrichText holds the HTML version. Email messages can be cut to the latest reply: see truncationStatus.
userIdthread associatedContactIdHubSpot’s internal contact id. Fall back to the thread id when there’s no contact.
incomingmessage directiontrue when direction is INCOMING, false for OUTGOING.
sessionIdthread idPrefix it, for example hubspot-176347007.
conversationIdthread idSame value as sessionId.
dimensionlabs_timestampmessage createdAtConvert with toMillis.
platformJsonthread inboxId, status, threadAssociations.associatedTicketId; message channelIdThe ticket id lets you join to HubSpot ticket data. Leave out senders: it can hold names and email addresses.

Personal data. Messages can hold names, emails and phone numbers: redact them before you send. See PII redaction.

// hubspot.mjs. Environment: HUBSPOT_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 base = 'https://api.hubspot.com/conversations/v3/conversations';
// Retries HubSpot's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}` } });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`HubSpot returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
// Sends every message written at or after `since` (Unix ms). Returns the new `since`.
export async function syncHubSpot({ since, historical = false }) {
let newest = since;
let after = new Date(since).toISOString();
for (;;) {
const page = await get(`${base}/threads?sort=latestMessageTimestamp&latestMessageTimestampAfter=${after}&limit=500&association=TICKET`);
for (const thread of page.results) {
for (let next = ''; next !== null; ) {
const res = await get(`${base}/threads/${thread.id}/messages${next ? `?after=${next}` : ''}`);
for (const m of res.results) {
const ts = toMillis(m.createdAt);
if (m.type !== 'MESSAGE' || ts < since || !m.text?.trim()) continue;
await sendToDimensionLabs({
text: m.text,
userId: thread.associatedContactId ? String(thread.associatedContactId) : `hubspot-${thread.id}`,
incoming: m.direction === 'INCOMING',
sessionId: `hubspot-${thread.id}`,
conversationId: `hubspot-${thread.id}`,
dimensionlabs_timestamp: ts,
platformJson: { inbox_id: thread.inboxId, status: thread.status, ticket_id: thread.threadAssociations?.associatedTicketId, channel_id: m.channelId },
}, { historical: historical || ts < Date.now() - HOUR });
newest = Math.max(newest, ts);
}
next = res.paging?.next?.after ?? null;
}
}
const last = page.results.at(-1)?.latestMessageTimestamp;
if (page.results.length < 500 || !last || last === after) return newest;
after = last;
}
}

Save the code as hubspot.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncHubSpot as sync } from './hubspot.mjs';.

  • History first: node run.mjs 2025-01-01 sends every message since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new messages 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.

You can also upload a file of exported conversations (check it with the file checker), then run a backfill.

Checked against HubSpot’s documentation on 2026-10-13.