# Twilio Conversations and Flex

> Send messages from Twilio Conversations, including Flex messaging, to Dimension Labs with the universal API.

This brings in the messages of your Twilio Conversations: SMS, WhatsApp, chat and the other channels it carries. Flex messaging channels run on Twilio Conversations, so the same steps cover Flex chats. You run it on your side with the [universal API](/api/tracker). In Twilio's docs the REST resources are listed under Twilio Conversations (classic).

## What you need

- A Twilio account that can create API keys.
- A Twilio API key (SID and secret). A Standard key works. A Restricted key needs read access to Conversations.
- The identities your agents and bots use in Conversations, so you can tell their messages from the customer's.
- 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 Twilio

Twilio's own steps are in [Create API keys in Twilio Console](https://www.twilio.com/docs/iam/api-keys/keys-in-console).

1. In Twilio Console, go to **Settings > Account settings > API keys & auth tokens**.
2. Select your region, then click **Create API key**.
3. Enter a name and choose **Standard** (or **Restricted**, and select its permissions). Click **Next**.
4. Copy the secret and store it in a secrets manager, select **Got it!** and click **Finish**.

Twilio uses HTTP Basic authentication: the API key SID is the username and the secret is the password.

## 2. Pull new messages

List conversations, then the messages of each one.

```bash
curl -u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
  "https://conversations.twilio.com/v1/Conversations?StartDate=2026-10-12T00:00:00Z&PageSize=100"

curl -u "$TWILIO_API_KEY_SID:$TWILIO_API_KEY_SECRET" \
  "https://conversations.twilio.com/v1/Conversations/CHXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages?Order=asc&PageSize=100"
```

- `StartDate` filters by the conversation's creation date and must start at the beginning of a day (`YYYY-MM-DDT00:00:00Z`). There is no "updated since" filter, so look back a few days and only send messages newer than your cursor.
- `State` filters by `active`, `inactive` or `closed`.
- `PageSize` goes up to 100 (default 50). Each response has `meta.next_page_url`. Request it until it is `null`.
- Messages default to `Order=asc` (oldest first).
- Twilio limits state-changing actions (default 30 per second). Twilio's limits page says read actions are not limited under that rule.

## 3. Map the fields

| Dimension Labs | Twilio | Notes |
|---|---|---|
| `text` | `body` | Up to 1,600 characters. Skip messages with no `body` (media only). |
| `userId` | conversation `sid` | On every message. Don't use the customer's `author`: it is their phone number, WhatsApp address or chat identity. To link one customer's conversations, look up their participant in `GET /v1/Conversations/{sid}/Participants` and send a hash of the address instead. |
| `incoming` | `author` | Twilio has no field that marks the customer. `author` is the participant's channel identifier and defaults to `system`. Treat a message as `incoming: false` when `author` is `system` or one of your agent or bot identities. |
| `sessionId` | conversation `sid` | Starts with `CH`. |
| `conversationId` | conversation `sid` | |
| `dimensionlabs_timestamp` | `date_created` | Convert with `toMillis`. |
| `platformJson` | `index`, `participant_sid`, conversation `friendly_name`, `state`, `attributes` | `attributes` is a JSON string you set; parse it for channel or account ids. |

**Personal data.** Never send `author` for customer messages. Message text can hold names, addresses and card details: redact it before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// twilio.mjs. Environment: TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, TWILIO_AGENT_IDENTITIES
// (a comma-separated list of the identities your agents and bots use), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const LOOKBACK_DAYS = 3; // raise it if your conversations stay open longer
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const auth = 'Basic ' + Buffer.from(`${process.env.TWILIO_API_KEY_SID}:${process.env.TWILIO_API_KEY_SECRET}`).toString('base64');
const AGENTS = new Set((process.env.TWILIO_AGENT_IDENTITIES ?? '').split(',').filter(Boolean));

// Retries Twilio's rate limits (429) and server errors (5xx) with a growing pause.
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { Authorization: auth } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Twilio returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}
async function* pages(url, key) {
  for (let next = url; next; ) { const p = await get(next); yield* p[key]; next = p.meta.next_page_url; }
}

// Sends every message created at or after `since` (Unix ms). Returns the new `since`.
export async function syncTwilio({ since, historical = false }) {
  const day = new Date(since - LOOKBACK_DAYS * 24 * HOUR).toISOString().slice(0, 10);
  let newest = since;
  const convos = `https://conversations.twilio.com/v1/Conversations?StartDate=${day}T00:00:00Z&PageSize=100`;
  for await (const c of pages(convos, 'conversations')) {
    const msgs = `https://conversations.twilio.com/v1/Conversations/${c.sid}/Messages?Order=asc&PageSize=100`;
    for await (const m of pages(msgs, 'messages')) {
      const ts = toMillis(m.date_created);
      if (ts < since || !m.body?.trim()) continue;
      const fromUs = m.author === 'system' || AGENTS.has(m.author);
      await sendToDimensionLabs({
        text: m.body,
        userId: c.sid,
        incoming: !fromUs,
        sessionId: c.sid,
        conversationId: c.sid,
        dimensionlabs_timestamp: ts,
        platformJson: { index: m.index, participant_sid: m.participant_sid, state: c.state, friendly_name: c.friendly_name },
      }, { historical: historical || ts < Date.now() - HOUR });
      newest = Math.max(newest, ts);
    }
  }
  return newest;
}
```

`StartDate` filters by when a conversation was created, so each run also looks back `LOOKBACK_DAYS` days for conversations that are still getting messages. If you set `friendly_name` to something that names the customer, leave it out of `platformJson`.

## 5. Run it

Save the code as `twilio.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 { syncTwilio as sync } from './twilio.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.

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

## Twilio reference

- [Create API keys in Twilio Console](https://www.twilio.com/docs/iam/api-keys/keys-in-console)
- [API keys overview](https://www.twilio.com/docs/iam/api-keys)
- [Twilio API requests](https://www.twilio.com/docs/usage/requests-to-twilio)
- [Conversation resource](https://www.twilio.com/docs/conversations/api/conversation-resource)
- [Conversation Message resource](https://www.twilio.com/docs/conversations/api/conversation-message-resource)
- [Conversation Participant resource](https://www.twilio.com/docs/conversations/api/conversation-participant-resource)
- [Conversations (classic) limits](https://www.twilio.com/docs/conversations/conversations-limits)
- [Flex Conversations](https://www.twilio.com/docs/flex/conversations)

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