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.
- HH HubSpotTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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.readscope. - A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in HubSpot
Section titled “1. Get API access in HubSpot”HubSpot calls private apps “legacy apps”: still supported, without the newest app features (private apps).
- In HubSpot, go to Development, then Legacy apps in the left sidebar.
- In the top right, click Create legacy app and select Private.
- Name the app, then on the Scopes tab add
conversations.read. - Click Create app.
- Open the app, go to the Auth tab, click Show token and copy it.
- Send it as
Authorization: Bearer YOUR_TOKENon every call.
2. Pull new threads
Section titled “2. Pull new threads”Retrieve threads sorted by their latest message, starting from your cursor. association=TICKET adds the ticket id to threads that have one.
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).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | HubSpot | Notes |
|---|---|---|
text | message text | richText holds the HTML version. Email messages can be cut to the latest reply: see truncationStatus. |
userId | thread associatedContactId | HubSpot’s internal contact id. Fall back to the thread id when there’s no contact. |
incoming | message direction | true when direction is INCOMING, false for OUTGOING. |
sessionId | thread id | Prefix it, for example hubspot-176347007. |
conversationId | thread id | Same value as sessionId. |
dimensionlabs_timestamp | message createdAt | Convert with toMillis. |
platformJson | thread inboxId, status, threadAssociations.associatedTicketId; message channelId | The 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.
4. Send it
Section titled “4. Send it”// 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; }}5. Run it
Section titled “5. Run it”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-01sends 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.
HubSpot reference
Section titled “HubSpot reference”- Conversations inbox and messages API guide
- Get a single message (message fields)
- Private apps
- API usage guidelines and limits
- Developer platform and API versioning
Checked against HubSpot’s documentation on 2026-10-13.