# Front

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

This brings in the messages of your Front conversations, inbound from customers and outbound from your team, one conversation per Front conversation. You run it on your side: a small script reads Front's events feed and posts each message to the [universal API](/api/tracker).

## What you need

- A Front admin: only admins can create or manage API tokens.
- A Front API token with read access to the inboxes you want to send.
- 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 Front

1. Go to **Settings > Developers**.
2. Open the **API Tokens** tab and click **Create API token**.
3. Give it a descriptive name.
4. Choose the **Access resources** feature, the namespaces it covers (for example **All shared workspaces**), and **Read** permission. Private inboxes are only included if the teammate has allowed API access in their settings.
5. Click **Create** and copy the token. You can't view it again later ([API tokens](https://dev.frontapp.com/docs/create-and-revoke-api-tokens)).
6. Send it as `Authorization: Bearer YOUR_TOKEN` on every call.

## 2. Pull new messages

[List events](https://dev.frontapp.com/reference/list-events) filtered to message events after your cursor. `inbound` is a message received, `outbound` a message sent and `out_reply` a reply sent. Each event carries the message as `target.data` and its `conversation` ([events](https://dev.frontapp.com/reference/events)).

```bash
curl -H "Authorization: Bearer $FRONT_TOKEN" \
  "https://api2.frontapp.com/events?q[types]=inbound&q[types]=outbound&q[types]=out_reply&q[after]=1760313600&sort_by=created_at&sort_order=asc"
```

`q[after]` is a Unix timestamp in seconds and filters on when the event was emitted (`emitted_at`), which for imported messages can be later than the message's own `created_at`. This endpoint returns at most 15 events per page. Follow `_pagination.next` until it is `null` ([pagination](https://dev.frontapp.com/docs/pagination), [query object](https://dev.frontapp.com/docs/query-object-q)).

To read one conversation in full, [list conversation messages](https://dev.frontapp.com/reference/list-conversation-messages): `GET /conversations/{conversation_id}/messages`.

Rate limits, per Front, are per company: 50 requests per minute on Starter, 100 on Professional and 200 on Enterprise. Responses carry `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` headers ([rate limits](https://dev.frontapp.com/docs/rate-limiting)).

## 3. Map the fields

| Dimension Labs | Front | Notes |
|---|---|---|
| `text` | message `text`, else `body` with HTML stripped | `text` is the plain version for email messages. `body` can be HTML. |
| `userId` | the conversation's contact id | The id at the end of `recipient._links.related.contact` (it starts with `crd_`), on every message of the conversation. Fall back to the conversation `id`. Don't use `recipient.handle`: it is the customer's email address or phone number. |
| `incoming` | message `is_inbound` | `true` for a message received, `false` for one sent. |
| `sessionId` | conversation `id` | Front ids such as `cnv_yo1kg5q` have no colons. |
| `conversationId` | conversation `id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | message `created_at` | Unix seconds, with a fraction. `toMillis` turns them into whole milliseconds. |
| `platformJson` | message `type`; conversation `tags`, `status`, `ticket_ids`, `custom_fields` | `type` is the channel, such as `email`, `sms` or `front_chat`. |

**Personal data.** Email bodies hold signatures, addresses and phone numbers: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// front.mjs. Environment: FRONT_TOKEN, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const stripHtml = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
const types = ['inbound', 'outbound', 'out_reply'].map((t) => `q[types]=${t}`).join('&');

// Retries Front'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: { Authorization: `Bearer ${process.env.FRONT_TOKEN}` } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Front returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every message event emitted at or after `since` (Unix ms). Returns the new `since`.
export async function syncFront({ since, historical = false }) {
  let newest = since;
  let url = `https://api2.frontapp.com/events?${types}&q[after]=${Math.floor(since / 1000) - 1}&sort_by=created_at&sort_order=asc`;
  while (url) {
    const page = await get(url);
    for (const event of page._results) {
      const m = event.target?.data;
      const conv = event.conversation;
      newest = Math.max(newest, toMillis(event.emitted_at));
      const text = m?.text || (m?.body ? stripHtml(m.body) : '');
      if (!text) continue;
      const ts = toMillis(m.created_at);
      const contact = conv.recipient?._links?.related?.contact?.split('/').pop();
      await sendToDimensionLabs({
        text,
        userId: contact || conv.id,
        incoming: m.is_inbound,
        sessionId: conv.id,
        conversationId: conv.id,
        dimensionlabs_timestamp: ts,
        platformJson: { channel: m.type, status: conv.status, tags: conv.tags?.map((t) => t.name), ticket_ids: conv.ticket_ids, custom_fields: conv.custom_fields },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
    url = page._pagination?.next;
  }
  return newest;
}
```

The cursor follows `emitted_at`, so an imported message whose `created_at` is old still gets picked up; it goes to `/trackhistorical` because of its age.

## 5. Run it

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

- **History first:** `node run.mjs 2025-01-01` sends every message event 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.

At 15 events per page and 50 requests a minute on Starter, a history load sends about 750 messages a minute at most; the code waits whenever Front asks it to. You can also [upload a file](/data/upload-a-file) of exported messages (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Front reference

- [API tokens](https://dev.frontapp.com/docs/create-and-revoke-api-tokens)
- [Events](https://dev.frontapp.com/reference/events)
- [List events](https://dev.frontapp.com/reference/list-events)
- [List conversation messages](https://dev.frontapp.com/reference/list-conversation-messages)
- [Query object 'q'](https://dev.frontapp.com/docs/query-object-q)
- [Pagination](https://dev.frontapp.com/docs/pagination)
- [Rate limits](https://dev.frontapp.com/docs/rate-limiting)

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