# Drift

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

This brings in each Drift conversation as one conversation: what the site visitor (the contact) wrote and what your team and bots replied. You run it on your side: a small script pulls from the Drift API and posts each message to the [universal API](/api/tracker).

## What you need

- A Drift user who can create and install an app in your Drift organization from [dev.drift.com](https://dev.drift.com).
- An access token for that app with the `conversation_read` scope.
- 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 Drift

Drift's own steps are in [Setting up an integration](https://devdocs.drift.com/docs/quick-start).

1. Go to [dev.drift.com](https://dev.drift.com) and click **Build Your App** under **Your Apps**.
2. On the **OAuth & Permissions** page, add the `conversation_read` scope ([scopes](https://devdocs.drift.com/docs/authentication-and-scopes)).
3. Under **Activate Your App**, click **Install App to Drift**. This reveals a non-expiring access token for your own organization. Copy it.
4. Send it as `Authorization: Bearer YOUR_TOKEN`.

Tokens are bound to the scopes they were granted with. If you add a scope later, regenerate the token. You don't need to build the OAuth flow for an app that only reads your own organization.

## 2. Pull new conversations

Drift has no bulk export of conversation data. Its [export guide](https://devdocs.drift.com/docs/how-to-export-conversations-from-drift) says to list conversations, then fetch each one's messages.

[List conversations](https://devdocs.drift.com/docs/list-conversations). The list is ordered by most recently updated, so stop once you reach conversations older than your cursor:

```bash
curl -H "Authorization: Bearer $DRIFT_TOKEN" \
  "https://api.drift.com/conversations/list?limit=50"
```

Drift's pages give the maximum `limit` as 100 on one page and 50 on another, so the code uses 50. Each conversation has `id`, `status`, `contactId`, `inboxId`, `createdAt` and `updatedAt` (Unix milliseconds). Follow `links.next` for the next page.

For each conversation, [retrieve its messages](https://devdocs.drift.com/docs/retrieve-a-conversations-messages) with `GET https://driftapi.com/conversations/{conversationId}/messages`. Pages are always 50 messages. Pass the `pagination.next` value from the response as `next` to get the next page. Attachments are fetched separately.

Drift returns `429` with the error type `rate_limit` when you have used up your calls for the time window ([data and errors](https://devdocs.drift.com/docs/data-and-errors)). Its docs don't publish the numbers.

## 3. Map the fields

| Dimension Labs | Drift | Notes |
|---|---|---|
| `text` | message `body` | Drift allows `<b>`, `<em>` and `<a>` tags and HTML entities in `body`: the code strips them. Skip messages whose `type` is `private_note`: only your team can see those. |
| `userId` | conversation `contactId` | Drift's internal id for the contact, on every message of the conversation. Fall back to the `sessionId`. |
| `incoming` | message `author.type` | `true` when `author.type` is `contact`. Your team and bots are `user`. |
| `sessionId` | conversation `id` | Prefix it, for example `drift-3782727146`. |
| `conversationId` | conversation `id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | message `createdAt` | Convert with `toMillis`. |
| `platformJson` | conversation `status`, `inboxId`; message `author.bot` | `author.bot` tells bot replies apart from your team's. |

Bot messages shown before a visitor's first message can arrive inside that message's `attributes.preMessages` rather than as messages of their own. The code sends messages only.

**Personal data.** Visitors often type their name, email and phone number into chats: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// drift.mjs. Environment: DRIFT_TOKEN, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const headers = { Authorization: `Bearer ${process.env.DRIFT_TOKEN}` };
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'", nbsp: ' ' };
const plain = (body) => String(body ?? '').replace(/<[^>]+>/g, '').replace(/&(amp|lt|gt|quot|#39|nbsp);/g, (_, e) => ENTITIES[e]);

// Retries Drift's rate limits (429) and server errors (5xx), honoring Retry-After.
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Drift returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every chat message written at or after `since` (Unix ms). Returns the new `since`.
export async function syncDrift({ since, historical = false }) {
  const changed = (c) => toMillis(c.updatedAt) >= since;
  const conversations = [];
  for (let url = 'https://api.drift.com/conversations/list?limit=50'; url; ) {
    const page = await get(url);
    const items = page.data ?? [];
    conversations.push(...items.filter(changed));
    url = items.length && items.every(changed) ? page.links?.next : null; // newest first: stop at the cursor
  }
  let newest = since;
  for (const c of conversations) {
    const id = `drift-${c.id}`;
    for (let next = null; ; ) {
      const res = await get(`https://driftapi.com/conversations/${c.id}/messages${next ? `?next=${encodeURIComponent(next)}` : ''}`);
      const messages = res.data?.messages ?? [];
      for (const m of messages) {
        const ts = toMillis(m.createdAt);
        const text = plain(m.body);
        if (m.type !== 'chat' || ts < since || !text.trim()) continue;
        await sendToDimensionLabs({
          text,
          userId: c.contactId ? String(c.contactId) : id,
          incoming: m.author?.type === 'contact',
          sessionId: id,
          conversationId: id,
          dimensionlabs_timestamp: ts,
          platformJson: { status: c.status, inboxId: c.inboxId, bot: m.author?.bot ?? false },
        }, { historical: historical || ts < Date.now() - HOUR });
        newest = Math.max(newest, ts);
      }
      next = res.pagination?.next;
      if (!next || !messages.length) break;
    }
  }
  return newest;
}
```

## 5. Run it

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

- **History first:** `node run.mjs 2025-01-01` sends every message since that date to `/trackhistorical`. 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.

Each conversation costs at least one call, so a history load takes a while; the code waits whenever Drift returns `429`. You can also [upload a file](/data/upload-a-file) of exported conversations with one row per message (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Drift reference

- [Setting up an integration](https://devdocs.drift.com/docs/quick-start)
- [Authentication and scopes](https://devdocs.drift.com/docs/authentication-and-scopes)
- [How to export conversations from Drift](https://devdocs.drift.com/docs/how-to-export-conversations-from-drift)
- [List conversations](https://devdocs.drift.com/docs/list-conversations)
- [Retrieving a conversation's messages](https://devdocs.drift.com/docs/retrieve-a-conversations-messages)
- [Message model](https://devdocs.drift.com/docs/message-model)
- [Conversation model](https://devdocs.drift.com/docs/conversation-model)
- [Data and errors](https://devdocs.drift.com/docs/data-and-errors)

Checked against Drift's documentation on 2026-10-26.
