# Crisp

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

This brings in each Crisp conversation as one conversation: what the visitor wrote and what your operators and bots replied, across chat, email and the other channels connected to your inbox. You run it on your side: a small script pulls from the Crisp REST API and posts each message to the [universal API](/api/tracker).

## What you need

- The owner of your Crisp workspace: only owners can generate a website token.
- Your website token (an identifier and a key) and your website ID.
- 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 Crisp

Crisp's own steps are in [Website token](https://docs.crisp.chat/guides/rest-api/authentication/website-token/).

1. Open the Crisp Dashboard and go to **Settings > Workspace Settings > Advanced configuration**.
2. In the **API Token** section, click **Generate Token**.
3. Copy the token identifier and key. They are shown only once.
4. Copy your website ID from **Settings > Workspace Settings > Setup Instructions**.

Authenticate with Basic auth (the identifier as the username, the key as the password) and add the header `X-Crisp-Tier: website` to every request.

A website token reaches one workspace and allows 10,000 requests a day. For several workspaces or a higher quota, Crisp offers plugin tokens through its Marketplace ([authentication](https://docs.crisp.chat/guides/rest-api/authentication/)).

## 2. Pull new conversations

[List conversations](https://docs.crisp.chat/references/rest-api/v1/) updated in a time range. The page number is part of the path:

```bash
curl --user "$CRISP_TOKEN_ID:$CRISP_TOKEN_KEY" -H "X-Crisp-Tier: website" \
  "https://api.crisp.chat/v1/website/$CRISP_WEBSITE_ID/conversations/1?per_page=50&order_date_updated=1&filter_date_start=2026-10-01T00:00:00.000Z&filter_date_end=2026-10-26T00:00:00.000Z"
```

- `per_page` is between 20 and 50.
- `filter_date_start` and `filter_date_end` take ISO 8601 dates and apply to when the conversation was last updated.
- Each conversation has a `session_id` and a `people_id`.

For each conversation, get its messages with `GET /v1/website/{website_id}/conversation/{session_id}/messages`. It returns the latest batch of messages. To page back to older ones, pass the oldest `timestamp` you have as `timestamp_before`.

Rate limits, per Crisp: on top of the website token's daily quota, the API applies global and per-route limits. Going over returns `429 Too Many Requests` or `420 Enhance Your Calm` ([rate limits](https://docs.crisp.chat/guides/rest-api/rate-limits/)).

## 3. Map the fields

| Dimension Labs | Crisp | Notes |
|---|---|---|
| `text` | message `content` | Only messages whose `type` is `text`. Skip `note` messages: those are private notes for your team. Files, pickers and events are skipped too. |
| `userId` | conversation `people_id` | Crisp's internal id for the contact, on every message of the conversation. Fall back to the `sessionId`. |
| `incoming` | message `from` | `true` when `from` is `user` (the visitor). Operators and bots are `operator`. |
| `sessionId` | conversation `session_id` | Prefix it, for example `crisp-session_700c65e1-...`. |
| `conversationId` | conversation `session_id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | message `timestamp` | Convert with `toMillis`. |
| `platformJson` | message `origin`, `automated`; conversation `state`, `inbox_id`, `meta.segments` | `automated` is `true` for bot messages. |

**Personal data.** Don't send the `nickname`, `email`, `phone`, `address` or `ip` from the conversation's `meta`. Messages can hold the same details: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// crisp.mjs. Environment: CRISP_WEBSITE_ID, CRISP_TOKEN_ID, CRISP_TOKEN_KEY, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = `https://api.crisp.chat/v1/website/${process.env.CRISP_WEBSITE_ID}`;
const headers = {
  Authorization: 'Basic ' + Buffer.from(`${process.env.CRISP_TOKEN_ID}:${process.env.CRISP_TOKEN_KEY}`).toString('base64'),
  'X-Crisp-Tier': 'website',
};

// Retries Crisp's rate limits (429 and 420) 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();
    const retry = res.status === 429 || res.status === 420 || res.status >= 500;
    if (!retry || attempt === 5) throw new Error(`Crisp returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Reads a conversation's messages, newest batch first, back to `since`.
async function messagesSince(sessionId, since) {
  const all = [];
  for (let before = null; ; ) {
    const res = await get(`${base}/conversation/${sessionId}/messages${before ? `?timestamp_before=${before}` : ''}`);
    const batch = res.data ?? [];
    if (!batch.length) break;
    all.push(...batch);
    const oldest = Math.min(...batch.map((m) => m.timestamp));
    if (oldest < since) break;
    before = oldest;
  }
  return all;
}

// Sends every message written at or after `since` (Unix ms). Returns the new `since`.
export async function syncCrisp({ since, historical = false }) {
  const range = `filter_date_start=${encodeURIComponent(new Date(since).toISOString())}&filter_date_end=${encodeURIComponent(new Date().toISOString())}`;
  let newest = since;
  for (let page = 1; ; page++) {
    const conversations = (await get(`${base}/conversations/${page}?per_page=50&order_date_updated=1&${range}`)).data ?? [];
    if (!conversations.length) break;
    for (const c of conversations) {
      const id = `crisp-${c.session_id}`;
      for (const m of await messagesSince(c.session_id, since)) {
        const ts = toMillis(m.timestamp);
        if (m.type !== 'text' || typeof m.content !== 'string' || ts < since || !m.content.trim()) continue;
        await sendToDimensionLabs({
          text: m.content,
          userId: c.people_id || id,
          incoming: m.from === 'user',
          sessionId: id,
          conversationId: id,
          dimensionlabs_timestamp: ts,
          platformJson: { origin: m.origin, automated: m.automated ?? false, state: c.state, inbox_id: c.inbox_id, segments: c.meta?.segments },
        }, { historical: historical || ts < Date.now() - HOUR });
        newest = Math.max(newest, ts);
      }
    }
  }
  return newest;
}
```

## 5. Run it

Save the code as `crisp.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 { syncCrisp as sync } from './crisp.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 request, so the website token's 10,000 requests a day limit how much history one day's run can load. If a history load stops at the quota, run it again the next day with the same date: what was already sent is deduplicated. 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.

## Crisp reference

- [Authentication](https://docs.crisp.chat/guides/rest-api/authentication/)
- [Website token](https://docs.crisp.chat/guides/rest-api/authentication/website-token/)
- [Rate limits](https://docs.crisp.chat/guides/rest-api/rate-limits/)
- [REST API reference (List conversations, Get messages in conversation)](https://docs.crisp.chat/references/rest-api/v1/)

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