# Aircall

> Send call and voicemail transcripts from Aircall to Dimension Labs with the universal API.

This brings in Aircall call transcripts, and voicemail transcripts, turn by turn, as Aircall AI transcribed them. One call becomes one conversation in Dimension Labs. You run it on your side: a small script pulls from the Aircall API and posts each message to the [universal API](/api/tracker).

## What you need

- **The Aircall AI package, with AI Assist or AI Assist Pro.** Transcripts are part of Aircall's Conversation Intelligence, and Aircall only returns them to companies that have signed up for that package. Without it there is no transcript to pull. Check your plan with your Aircall account manager.
- An Aircall API ID and API token, from your company's settings.
- 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 Aircall

1. Open your company's **Settings** page in the Aircall Dashboard. In the **API Keys** section, select **Add a new API key**.
2. Copy the API ID and the API token. Aircall doesn't store the token in plain text, so it can't show it to you again.
3. Send them with HTTP Basic authentication: the API ID is the username and the API token is the password, as `Authorization: Basic BASE64(api_id:api_token)`.

OAuth is only for apps built for several Aircall companies; you don't need it for your own account.

## 2. Pull new calls

List calls, then get each call's transcription:

```bash
curl -u "$AIRCALL_API_ID:$AIRCALL_API_TOKEN" \
  "https://api.aircall.io/v1/calls?from=1761436800&to=1761523200&order=asc&per_page=50&fetch_contact=true"

curl -u "$AIRCALL_API_ID:$AIRCALL_API_TOKEN" \
  "https://api.aircall.io/v1/calls/CALL_ID/transcription"
```

- `from` and `to` filter calls by when they were created, in UNIX seconds. The code asks one day at a time, from `AIRCALL_LOOKBACK_HOURS` (24 by default) before the cursor, and keeps calls that ended since the last run (`ended_at`, with `status` `done`).
- Pages hold up to 50 calls (`per_page`). Follow `meta.next_page_link` until it is `null`. A single list stops at 10,000 calls even with paging, which is why the code asks one day at a time.
- Only six months of call history is available. For older calls, Aircall's Support team can make a one-time export.
- `fetch_contact=true` adds the contact to each call. Without it, `contact` is left out.
- The transcription has `type` (`call` or `voicemail`) and `content.utterances[]`, each with `text`, `start_time`, `end_time` and `participant_type`: `internal` is an Aircall user (your side, with a `user_id`), `external` is the other party (with a `phone_number`). A call with no transcription returns `404`.
- Use the transcription endpoint, not `realtime_transcription`, which Aircall is retiring.

## 3. Map the fields

| Dimension Labs | Aircall | Notes |
|---|---|---|
| `text` | `content.utterances[].text` | Skip empty utterances. |
| `userId` | call `contact.id` | Aircall's id for the contact, with `fetch_contact=true`. If the call has no contact, the code uses the `sessionId`. Never the phone number. |
| `incoming` | `utterances[].participant_type` | `true` when `external`. `internal` (an Aircall user) is your side. |
| `sessionId` | call `id` | One call is one conversation in Dimension Labs. |
| `conversationId` | call `id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | call `started_at` plus `utterances[].start_time` | `started_at` is UNIX seconds. In Aircall's example, `start_time` is seconds into the call, so the code adds it to `started_at`. Turns closer than a second apart are spaced to 1,000 ms. |
| `platformJson` | transcription `type`, `content.language`, call `direction`, `number.id`, tag names | Leave out `raw_digits`, `phone_number`, the contact's details and the user's name and email: they are phone numbers, names and addresses. |

**Personal data.** Transcripts hold what people say: names, phone numbers, card and account numbers. Each external utterance carries the caller's `phone_number`, so the code never sends it. Redact the text before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// aircall.mjs. Environment: AIRCALL_API_ID, AIRCALL_API_TOKEN, AIRCALL_LOOKBACK_HOURS (default 24), AIRCALL_DELAY_MINUTES (default 30), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = 'https://api.aircall.io/v1';
const AUTH = 'Basic ' + Buffer.from(`${process.env.AIRCALL_API_ID}:${process.env.AIRCALL_API_TOKEN}`).toString('base64');
const LOOKBACK = Number(process.env.AIRCALL_LOOKBACK_HOURS ?? 24) * HOUR;
const DELAY = Number(process.env.AIRCALL_DELAY_MINUTES ?? 30) * 60 * 1000;

// Retries rate limits (429) and server errors (5xx). Returns null on 404 (a call with no transcription).
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 === 404) return null;
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Aircall returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || Math.min(60, 2 ** attempt)) * 1000);
  }
}

// Calls created in [from, to), one day at a time: a single list stops at 10,000 calls.
async function* callsCreated(from, to) {
  for (let day = from; day < to; day += DAY) {
    const q = new URLSearchParams({
      from: String(Math.floor(day / 1000)),
      to: String(Math.floor(Math.min(day + DAY, to) / 1000)),
      order: 'asc',
      per_page: '50',
      fetch_contact: 'true',
    });
    let url = `${API}/calls?${q}`;
    while (url) {
      const page = await get(url);
      yield* page?.calls ?? [];
      url = page?.meta?.next_page_link ?? null;
    }
  }
}

// Sends every call that ended at or after `since` (Unix ms). Returns the new `since`.
export async function syncAircall({ since, historical = false }) {
  const until = Date.now() - DELAY; // give Aircall AI time to write the transcription
  if (until <= since) return since;
  for await (const c of callsCreated(since - LOOKBACK, until)) {
    if (c.status !== 'done' || !c.ended_at) continue;
    const ended = toMillis(c.ended_at);
    if (ended < since || ended >= until) continue;
    const found = await get(`${API}/calls/${c.id}/transcription`);
    const t = found?.transcription;
    if (!t) continue;
    const sessionId = `aircall-${c.id}`;
    const userId = c.contact?.id ? `aircall-${c.contact.id}` : sessionId;
    const start = toMillis(c.started_at);
    let last = 0;
    for (const u of t.content?.utterances ?? []) {
      if (!u.text?.trim()) continue;
      const ts = Math.max(start + Math.round((u.start_time ?? 0) * 1000), last + 1000); // keep turns at least a second apart
      last = ts;
      await sendToDimensionLabs({
        text: u.text,
        userId,
        incoming: u.participant_type === 'external',
        sessionId,
        conversationId: sessionId,
        dimensionlabs_timestamp: ts,
        platformJson: { type: t.type, direction: c.direction, language: t.content?.language, numberId: c.number?.id, tags: (c.tags ?? []).map((x) => x.name) },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
  }
  return until;
}
```

## 5. Run it

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

- **History first:** `node run.mjs 2026-05-01` sends every call since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab. The API only reaches back six months.
- **Then every 15 minutes:** the scheduled job sends calls that ended since the last run to `/track`, and they are enriched automatically every day. The code waits `AIRCALL_DELAY_MINUTES` (30 by default) after a call ends, so its transcription is ready. A call whose transcription isn't ready by then is skipped, so raise the delay if yours take longer. Turns more than an hour old by then go to `/trackhistorical`, and need a backfill.
- **Re-runs are safe:** duplicates are removed automatically.
- **Rate limits:** Aircall allows 120 requests a minute per company. Each call costs one transcription request, so a long history load takes a while. The code waits whenever Aircall returns `429`. Aircall Support can raise the limit.

Without the AI package, or for calls older than six months, there is no transcript to pull through the API. You can still [upload a file](/data/upload-a-file) of transcripts you have, check it first with the [file checker](/data/check-a-file), then run a backfill.

## Aircall reference

- [Aircall API reference](https://developer.aircall.io/api-references/)
- [Basic Auth for Aircall customers](https://developer.aircall.io/api-references/#basic-auth-aircall-customers)
- [Rate limiting](https://developer.aircall.io/api-references/#rate-limiting)
- [Pagination](https://developer.aircall.io/api-references/#pagination)
- [List all Calls](https://developer.aircall.io/api-references/#list-all-calls)
- [Conversation Intelligence overview](https://developer.aircall.io/api-references/#conversation-intelligence-overview)
- [Retrieve a transcription](https://developer.aircall.io/api-references/#retrieve-a-transcription)

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