# LiveChat

> Send LiveChat chats and transcripts to Dimension Labs with the universal API, on a schedule, with history and backfill.

This brings in your LiveChat chats, one message at a time: what each customer wrote and what your agents and bots replied. You run it on your side with the [universal API](/api/tracker), using LiveChat's Agent Chat Web API.

## What you need

- A LiveChat account that can sign in to the Developer Console. A token runs with the permissions of the account that created it, so use an account that can see the chats you want.
- A Personal Access Token with the `chats--all:ro` scope (or `chats--access:ro` for only the groups the account belongs to).
- 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 LiveChat

LiveChat's own steps are in [Personal access tokens](https://platform.text.com/docs/authorization/personal-access-tokens).

1. Open the Developer Console and go to **Settings > Authorization > Personal Access Tokens**.
2. Select the scopes. For this guide, select `chats--all:ro`. You can't change scopes after you create the token.
3. Select **Create token +** and copy the Personal Access Token, your Account ID and the Base64 encoded token.

Personal Access Tokens use Basic authentication: the username is your Account ID and the password is the token. Send the Base64 encoded value as `Authorization: Basic YOUR_BASE64_TOKEN`.

## 2. Pull new chats

Use the `list_archives` method. It returns one entry per thread, with that thread's events (messages) included.

```bash
curl -X POST https://api.livechatinc.com/v3.6/agent/action/list_archives \
  -H "Authorization: Basic $LIVECHAT_BASE64_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filters":{"from":"2026-10-13T00:00:00.000000+00:00","event_types":{"values":["message"]}},"sort_order":"asc","limit":100}'
```

- `filters.from` and `filters.to` take RFC 3339 dates with microseconds.
- `sort_order` is `asc` (oldest first) or `desc` (the default). `limit` goes up to 100.
- Each response may include `next_page_id`. Send it as `page_id` in the next request, with no `filters`, `limit` or `sort_order`: LiveChat keeps the filters from the first request. A `page_id` expires after one month.
- `thread.active` is `true` while a chat is still going. The code below skips those threads and picks them up on a later run, once they have ended.
- LiveChat applies rate limits and returns a "Too many requests" error when you exceed them. Its docs don't publish the numbers.

## 3. Map the fields

| Dimension Labs | LiveChat | Notes |
|---|---|---|
| `text` | `thread.events[].text` | Only events with `type` `message`. |
| `userId` | the user in `chats[].users` with `type` `customer` | LiveChat's internal customer `id`, on every message of the chat. Don't use the customer's `email` or `name`. |
| `incoming` | `events[].author_id` | `true` when the author is a user whose `type` is `customer`. Agents and bots have `type` `agent`. |
| `sessionId` | `thread.id` | One thread is one conversation. |
| `conversationId` | `chats[].id` | Groups every thread of the same chat. |
| `dimensionlabs_timestamp` | `events[].created_at` | Convert with `toMillis`. |
| `platformJson` | `thread.access.group_ids`, `thread.customer_visit.geolocation.country_code`, `chats[].properties`, `thread.properties` | Group ids are useful for joins by team or brand. |

**Personal data.** Customers often type names, emails and order numbers into chats: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// livechat.mjs. Environment: LIVECHAT_BASE64_TOKEN, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const ENDPOINT = 'https://api.livechatinc.com/v3.6/agent/action/list_archives';
const headers = { Authorization: `Basic ${process.env.LIVECHAT_BASE64_TOKEN}`, 'Content-Type': 'application/json' };
const rfc3339 = (ms) => new Date(ms).toISOString().replace('Z', '000+00:00'); // microseconds, as LiveChat asks

// Retries LiveChat's "Too many requests" (429) and server errors (5xx) with a growing pause.
async function post(body) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(ENDPOINT, { method: 'POST', headers, body: JSON.stringify(body) });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`LiveChat returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every ended chat thread started at or after `since` (Unix ms). Returns the new `since`.
export async function syncLiveChat({ since, historical = false }) {
  let body = { filters: { from: rfc3339(since), event_types: { values: ['message'] } }, sort_order: 'asc', limit: 100 };
  let newest = since;
  let holdAt = Infinity; // the start of the oldest chat that is still going
  while (body) {
    const page = await post(body);
    for (const chat of page.chats ?? []) {
      const t = chat.thread;
      const started = toMillis(t.created_at);
      if (t.active && started > Date.now() - 24 * HOUR) { holdAt = Math.min(holdAt, started); continue; } // send it once it ends
      const customer = chat.users.find((u) => u.type === 'customer');
      for (const e of t.events ?? []) {
        if (e.type !== 'message' || !e.text?.trim()) continue;
        const ts = toMillis(e.created_at);
        await sendToDimensionLabs({
          text: e.text,
          userId: customer?.id ?? t.id,
          incoming: e.author_id === customer?.id,
          sessionId: t.id,
          conversationId: chat.id,
          dimensionlabs_timestamp: ts,
          platformJson: { group_ids: t.access?.group_ids, country: t.customer_visit?.geolocation?.country_code },
        }, { historical: historical || ts < Date.now() - HOUR });
      }
      newest = Math.max(newest, started);
    }
    body = page.next_page_id ? { page_id: page.next_page_id } : null;
  }
  return Math.min(newest, holdAt);
}
```

A chat that is still going is skipped, and the cursor stays at its start, so the next run picks it up once it has ended. Later chats that already ended are sent straight away; when the cursor is held, they come round again on the next run and the duplicates are removed automatically. A chat left open for more than a day is sent as it is, so one forgotten chat can't hold the cursor back.

## 5. Run it

Save the code as `livechat.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 { syncLiveChat as sync } from './livechat.mjs';`.

- **History first:** `node run.mjs 2025-01-01` sends every chat since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends ended chats to `/track`, and they are enriched automatically every day. Because a chat is sent once it ends, the first messages of a chat that ran for more than an hour go to `/trackhistorical`, and need a backfill.
- **Re-runs are safe:** duplicates are removed automatically.

You can also [upload a file](/data/upload-a-file) of exported chats and check it first with the [file checker](/data/check-a-file), then run a backfill.

## LiveChat reference

- [Personal access tokens](https://platform.text.com/docs/authorization/personal-access-tokens)
- [Agent Chat Web API v3.6](https://platform.text.com/docs/messaging/agent-chat-api)
- [Agent Chat API data structures](https://platform.text.com/docs/messaging/agent-chat-api/v3.6/data-structures)

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