02 Get your data in /Integrations
Kustomer
Send Kustomer conversations and their messages to Dimension Labs with the universal API.
- K KustomerTickets, 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 Kustomer conversation and its messages from every channel, customer and agent alike. You run it on your side: a small script pulls from the Kustomer API and posts each message to the universal API.
What you need
Section titled “What you need”- A Kustomer admin, to create an API key.
- A Kustomer API key with roles that allow search, conversation read and message read (for example
org.permission.search.create,org.permission.conversation.readandorg.permission.message.read). - A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Kustomer
Section titled “1. Get API access in Kustomer”- In Kustomer, go to Settings > Security > API Keys.
- Create a key, give it a descriptive label and choose its API roles.
- Copy the key.
- Send it as
Authorization: Bearer YOUR_API_KEYon every call (authentication and API keys).
2. Pull new conversations
Section titled “2. Pull new conversations”Get conversations has no “updated since” filter, so use customer search with a conversation query instead:
curl -X POST "https://api.kustomerapp.com/v1/customers/search?pageSize=100" \ -H "Authorization: Bearer $KUSTOMER_API_KEY" -H "Content-Type: application/json" \ -d '{"queryContext":"conversation","and":[{"conversation_updated_at":{"gte":"2026-10-13T00:00:00.000Z"}}]}'Search covers objects updated in the past 2 years; older data is in Kustomer’s Archive Search API. Search returns at most 100 pages. To read more, Kustomer suggests repeating the query from the last updated_at value you received. Follow links.next for the next page (pagination).
For each conversation, get messages by conversation: GET /v1/conversations/{id}/messages?pageSize=100&sort=asc.
Rate limits, per Kustomer, are per minute across all of your organization’s API keys: 300 on Professional (legacy), 500 on Business (legacy), 1,000 on Enterprise and 2,000 on Ultimate. Searches by API keys have their own limit of 100 per minute. Going over returns 429 (rate limiting).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Kustomer | Notes |
|---|---|---|
text | message attributes.preview | Kustomer’s reference returns the message text in preview and doesn’t say whether long messages are shortened, so compare a long email with the app before relying on it. |
userId | message relationships.customer.data.id | Kustomer’s internal customer id, on both their messages and the agents’ replies. |
incoming | message attributes.direction | true when direction is in, false when it is out. |
sessionId | conversation id | Kustomer ids have no colons. |
conversationId | conversation id | Same value as sessionId. |
dimensionlabs_timestamp | message attributes.sentAt, else attributes.createdAt | Convert with toMillis. |
platformJson | message attributes.channel; conversation attributes.status, tags, satisfaction | The customer id lets you join to customer data in Kustomer. |
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”// kustomer.mjs. Environment: KUSTOMER_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 api = 'https://api.kustomerapp.com';
// Retries Kustomer's rate limits (429) and server errors (5xx), honoring Retry-After when it is sent.async function call(path, body) { for (let attempt = 0; ; attempt++) { const res = await fetch(api + path, { method: body ? 'POST' : 'GET', headers: { Authorization: `Bearer ${process.env.KUSTOMER_API_KEY}`, 'Content-Type': 'application/json' }, body: body && JSON.stringify(body), }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Kustomer returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
async function sendConversation(conv, since, historical) { let newest = since; for (let next = `/v1/conversations/${conv.id}/messages?pageSize=100&sort=asc`; next; ) { const res = await call(next); for (const m of res.data) { const a = m.attributes; const ts = toMillis(a.sentAt ?? a.createdAt); if (ts < since || !a.preview?.trim()) continue; await sendToDimensionLabs({ text: a.preview, userId: m.relationships?.customer?.data?.id ?? conv.id, incoming: a.direction === 'in', sessionId: conv.id, conversationId: conv.id, dimensionlabs_timestamp: ts, platformJson: { channel: a.channel, status: conv.attributes?.status, tags: conv.attributes?.tags, satisfaction: conv.attributes?.satisfaction }, }, { historical: historical || ts < Date.now() - HOUR }); newest = Math.max(newest, ts); } next = res.links?.next; } return newest;}
// Sends every message written at or after `since` (Unix ms). Returns the new `since`.export async function syncKustomer({ since, historical = false }) { let newest = since; let from = since; for (;;) { const query = { queryContext: 'conversation', and: [{ conversation_updated_at: { gte: new Date(from).toISOString() } }], sort: [{ conversation_updated_at: 'asc' }] }; let page = '/v1/customers/search?pageSize=100'; let pages = 0; while (page) { const found = await call(page, query); for (const conv of found.data) { newest = Math.max(newest, await sendConversation(conv, since, historical)); if (conv.attributes?.updatedAt) from = Math.max(from, toMillis(conv.attributes.updatedAt)); } page = found.links?.next; if (page && ++pages === 100) break; // search stops at 100 pages: search again from the last update seen } if (!page) return newest; }}Search stops at 100 pages, so on a big history load the code searches again from the last updatedAt it saw, as Kustomer suggests. Conversations at the restart point can be sent twice; duplicates are removed automatically.
5. Run it
Section titled “5. Run it”Save the code as kustomer.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncKustomer as sync } from './kustomer.mjs';.
- History first:
node run.mjs 2025-01-01sends every message since that date to/trackhistorical. Search reaches back 2 years; older conversations need Kustomer’s Archive Search API or a file. 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.
Kustomer reference
Section titled “Kustomer reference”- Authentication and API keys
- Create customer search
- Get conversations
- Get messages by conversation
- Pagination
- Rate limiting
Checked against Kustomer’s documentation on 2026-10-13.