# Botpress

> Send bot conversations from Botpress to Dimension Labs with the universal API.

This brings in your Botpress bot conversations, message by message: what each user wrote and what the bot answered. Each Botpress conversation becomes one conversation. You run it on your side: a small script pulls from the Botpress API and posts each message to the [universal API](/api/tracker).

## What you need

- A Botpress account with access to the bot's workspace, to create a personal access token.
- The bot 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 Botpress

1. Generate a personal access token (PAT) in the **Profile Settings** section of your Botpress account ([authentication](https://botpress.com/docs/api-reference/authentication)). Store it in a secrets manager: it has the same access you have.
2. Open the bot's dashboard. The bot id is the letters and numbers right after `chatbots/` in the URL ([raw conversations](https://botpress.com/docs/studio/guides/advanced/exporting-data/exporting-raw-conversations-with-the-botpress-api)).

Send both on every request: `Authorization: Bearer YOUR_PAT` and `x-bot-id: YOUR_BOT_ID`.

## 2. Pull new messages

[List messages](https://botpress.com/docs/api-reference/runtime-api/openapi/listMessages) created since your last run, across all of the bot's conversations:

```bash
curl -H "Authorization: Bearer $BOTPRESS_TOKEN" -H "x-bot-id: $BOTPRESS_BOT_ID" \
  "https://api.botpress.cloud/v1/chat/messages?afterDate=2026-10-01T00:00:00.000Z"
```

- `afterDate` and `beforeDate` are ISO 8601 times in UTC, both inclusive. Add `conversationId` to list one conversation's messages.
- Messages come back newest first. Pass `meta.nextToken` as `nextToken` until the response has none ([pagination](https://botpress.com/docs/api-reference/pagination)).
- Each message has `direction` (`incoming` from the user, `outgoing` from the bot), `type`, `payload`, `userId`, `conversationId` and `createdAt`. The text is in `payload.text` for text messages, choice cards and quick replies. Images have no text.
- Botpress keeps conversations and messages for 90 days ([retention period](https://botpress.com/docs/studio/guides/advanced/retention-period)).

Botpress uses a sliding-window rate limiter. Going over returns `429`, and every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets) headers ([rate limiting](https://botpress.com/docs/api-reference/ratelimiting)).

## 3. Map the fields

| Dimension Labs | Botpress | Notes |
|---|---|---|
| `text` | `payload.text` | Skip messages without text (images, cards). |
| `userId` | `userId` of the conversation's incoming messages | The end user's Botpress id. The bot's messages can carry a different `userId`, so use the end user's on both sides. Fall back to the `sessionId`. |
| `incoming` | `direction` | `true` for `incoming`, `false` for `outgoing`. |
| `sessionId` | `conversationId` | Prefix it, for example `botpress-<conversationId>`. |
| `conversationId` | `conversationId` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `createdAt` | Convert with `toMillis`. |
| `platformJson` | `type`, `id` | `type` is the message type, such as `text`, `choice` or `quick_reply`. |

**Personal data.** Users sometimes type names, emails or order numbers: redact them before you send. See [PII redaction](/data/pii-redaction). Don't copy message `tags` into `platformJson` without checking them: channel integrations can put their own ids there.

## 4. Send it

```js
// botpress.mjs. Environment: BOTPRESS_TOKEN, BOTPRESS_BOT_ID, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const BASE = 'https://api.botpress.cloud/v1/chat';
const headers = { Authorization: `Bearer ${process.env.BOTPRESS_TOKEN}`, 'x-bot-id': process.env.BOTPRESS_BOT_ID };

// Retries Botpress's rate limits (429) and server errors (5xx), honoring Retry-After or RateLimit-Reset.
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(`Botpress returned ${res.status}: ${await res.text()}`);
    const wait = Number(res.headers.get('retry-after')) || Number(res.headers.get('ratelimit-reset')) || 2 ** attempt;
    await sleep(wait * 1000);
  }
}

// Reads every page of messages matching the query.
async function listMessages(query) {
  const messages = [];
  let nextToken;
  do {
    const q = new URLSearchParams(query);
    if (nextToken) q.set('nextToken', nextToken);
    const page = await get(`${BASE}/messages?${q}`);
    messages.push(...(page.messages ?? []));
    nextToken = page.meta?.nextToken;
  } while (nextToken);
  return messages;
}

// Sends every message created at or after `since` (Unix ms). Returns the new `since`.
export async function syncBotpress({ since, historical = false }) {
  let newest = since;
  const byConversation = new Map();
  for (const m of await listMessages({ afterDate: new Date(since).toISOString() })) {
    if (!byConversation.has(m.conversationId)) byConversation.set(m.conversationId, []);
    byConversation.get(m.conversationId).push(m);
  }
  for (const [conversationId, messages] of byConversation) {
    const id = `botpress-${conversationId}`.replace(/:/g, '-');
    // The end user is whoever sent the incoming messages. Look further back if this batch has none.
    let user = messages.find((m) => m.direction === 'incoming')?.userId;
    if (!user) user = (await listMessages({ conversationId })).find((m) => m.direction === 'incoming')?.userId;
    messages.sort((a, b) => toMillis(a.createdAt) - toMillis(b.createdAt));
    for (const m of messages) {
      const ts = toMillis(m.createdAt);
      newest = Math.max(newest, ts);
      const text = m.payload?.text;
      if (typeof text !== 'string' || !text.trim()) continue;
      await sendToDimensionLabs({
        text,
        userId: user ? String(user).replace(/:/g, '-') : id,
        incoming: m.direction === 'incoming',
        sessionId: id,
        conversationId: id,
        dimensionlabs_timestamp: ts,
        platformJson: { type: m.type, botpress_message_id: m.id },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
  }
  return newest;
}
```

## 5. Run it

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

- **History first:** `node run.mjs 2026-08-01` sends every message since that date to `/trackhistorical`. Botpress keeps messages for 90 days, so that is as far back as the API goes. 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.
- **Keep it running:** messages older than 90 days are deleted, so a longer gap can't be filled from the API.
- **Re-runs are safe:** duplicates are removed automatically.

A history load reads every message in one pass, then sends them conversation by conversation; the code waits whenever Botpress asks it to. You can also write the converted messages to a JSONL file instead of posting them, [upload it](/data/upload-a-file) (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Botpress reference

- [Authentication](https://botpress.com/docs/api-reference/authentication)
- [Raw conversations with the Botpress API](https://botpress.com/docs/studio/guides/advanced/exporting-data/exporting-raw-conversations-with-the-botpress-api)
- [listMessages](https://botpress.com/docs/api-reference/runtime-api/openapi/listMessages)
- [Pagination](https://botpress.com/docs/api-reference/pagination)
- [Rate limiting](https://botpress.com/docs/api-reference/ratelimiting)
- [Retention period](https://botpress.com/docs/studio/guides/advanced/retention-period)

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