# Kustomer

> Send Kustomer conversations and their messages to Dimension Labs with the universal API.

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](/api/tracker).

## 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.read` and `org.permission.message.read`).
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

## 1. Get API access in Kustomer

1. In Kustomer, go to **Settings > Security > API Keys**.
2. Create a key, give it a descriptive label and choose its API roles.
3. Copy the key.
4. Send it as `Authorization: Bearer YOUR_API_KEY` on every call ([authentication and API keys](https://developer.kustomer.com/kustomer-api-docs/reference/authentication)).

## 2. Pull new conversations

[Get conversations](https://developer.kustomer.com/kustomer-api-docs/reference/getconversations) has no "updated since" filter, so use [customer search](https://developer.kustomer.com/kustomer-api-docs/reference/customersearch) with a conversation query instead:

```bash
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](https://developer.kustomer.com/kustomer-api-docs/reference/pagination)).

For each conversation, [get messages by conversation](https://developer.kustomer.com/kustomer-api-docs/reference/getmessagesbyconversation): `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](https://developer.kustomer.com/kustomer-api-docs/reference/rate-limiting)).

## 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](/data/pii-redaction).

## 4. Send it

```js
// kustomer.mjs. Environment: KUSTOMER_API_KEY, DL_API_KEY.


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

Save the code as `kustomer.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncKustomer as sync } from './kustomer.mjs';`.

- **History first:** `node run.mjs 2025-01-01` sends 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](/dimensions/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](/data/upload-a-file) of exported conversations (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Kustomer reference

- [Authentication and API keys](https://developer.kustomer.com/kustomer-api-docs/reference/authentication)
- [Create customer search](https://developer.kustomer.com/kustomer-api-docs/reference/customersearch)
- [Get conversations](https://developer.kustomer.com/kustomer-api-docs/reference/getconversations)
- [Get messages by conversation](https://developer.kustomer.com/kustomer-api-docs/reference/getmessagesbyconversation)
- [Pagination](https://developer.kustomer.com/kustomer-api-docs/reference/pagination)
- [Rate limiting](https://developer.kustomer.com/kustomer-api-docs/reference/rate-limiting)

Checked against Kustomer's documentation on 2026-10-13.
