# Dialpad

> Send call transcripts from Dialpad to Dimension Labs with the universal API.

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

## What you need

- Dialpad AI transcripts on the calls you want. The API returns the Dialpad AI transcript of a call; a call Dialpad AI didn't transcribe has none.
- A Dialpad Company Admin to create an API key. Only Company Admins can create one.
- 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 Dialpad

1. Go to **Admin Settings > My Company > Authentication > API Keys** and select **Add Key**. Dialpad's steps are in [Manual API Key Generation](https://developers.dialpad.com/docs/api-key-generation).
2. Name the key, set when it expires, and under **Additional Scopes** add `calls:list`. Select **Save**.
3. Send the key as `Authorization: Bearer YOUR_API_KEY`.

A company-level key lists every call in the company. Dialpad warns that API keys are more powerful than passwords, so keep it in your secret store. OAuth is only for apps used across several Dialpad companies; you don't need it for your own.

## 2. Pull new calls

List calls, then get each call's transcript:

```bash
curl -H "Authorization: Bearer $DIALPAD_API_KEY" \
  "https://dialpad.com/api/v2/call?started_after=1761436800000"

curl -H "Authorization: Bearer $DIALPAD_API_KEY" \
  "https://dialpad.com/api/v2/transcripts/CALL_ID"
```

- `Call -- List` only returns calls that have already ended, most recent first by start time. `started_after` and `started_before` are UTC milliseconds since the epoch.
- Each response has a `cursor`: send it back as `cursor` for the next page.
- The list filters by start time, so the code looks back `DIALPAD_LOOKBACK_HOURS` (24 by default) before the cursor and keeps calls that ended since the last run (`date_ended`).
- Calls tied to deleted users are anonymized and left out unless you add `include_anonymized=true`.
- The transcript has `lines[]`, each with `content`, `time`, `type`, and either `user_id` (a Dialpad user, your side) or `contact_id` (the other party). `type` is `transcript` for spoken lines; `moment`, `custom_moment`, `real_time_moment` and `ai_question` lines are Dialpad AI tags, not speech.

## 3. Map the fields

| Dimension Labs | Dialpad | Notes |
|---|---|---|
| `text` | `lines[].content` | Only lines with `type` `transcript`. |
| `userId` | call `contact.id` | Dialpad's id for the contact. If the call has none, the code uses the `sessionId`. |
| `incoming` | `lines[].user_id` | `false` when the line has a `user_id` (a Dialpad user: an agent, or a bot). `true` otherwise. |
| `sessionId` | `call_id` | One call is one conversation in Dimension Labs. |
| `conversationId` | `call_id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `lines[].time` | The time has no zone; the code reads it as UTC. Lines closer than a second apart are spaced to 1,000 ms. |
| `platformJson` | `direction`, `group_id`, `master_call_id`, `labels`, `csat_score`, moment names | Leave out `name`, `contact.name`, `contact.phone`, `contact.email`, `external_number` and `internal_number`: they are names and phone numbers. |

**Personal data.** Transcripts hold what people say: names, phone numbers, card and account numbers. Each line's `name` is the speaker's name or phone number, so the code never sends it. Redact the text before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// dialpad.mjs. Environment: DIALPAD_API_KEY (a company API key with the calls:list scope), DIALPAD_LOOKBACK_HOURS (default 24), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const API = 'https://dialpad.com/api/v2';
const LOOKBACK = Number(process.env.DIALPAD_LOOKBACK_HOURS ?? 24) * HOUR;

// Retries rate limits (429) and server errors (5xx). Returns null on 404 (a call with no transcript).
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.DIALPAD_API_KEY}` } });
    if (res.ok) return res.json();
    if (res.status === 404) return null;
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Dialpad returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every call that ended at or after `since` (Unix ms). Returns the new `since`.
export async function syncDialpad({ since, historical = false }) {
  let newest = since;
  let cursor;
  do {
    // Calls are listed by start time, so look back far enough to catch calls that started before `since`.
    const q = new URLSearchParams({ started_after: String(since - LOOKBACK) });
    if (cursor) q.set('cursor', cursor);
    const page = await get(`${API}/call?${q}`);
    for (const c of page?.items ?? []) {
      const ended = toMillis(c.date_ended ?? c.date_started);
      if (ended < since) continue;
      const transcript = await get(`${API}/transcripts/${c.call_id}`);
      const lines = transcript?.lines ?? [];
      const moments = lines.filter((l) => l.type !== 'transcript').map((l) => l.content);
      const sessionId = `dialpad-${c.call_id}`;
      const userId = c.contact?.id ? `dialpad-${c.contact.id}` : sessionId;
      let last = 0;
      for (const l of lines) {
        if (l.type !== 'transcript' || !l.content?.trim()) continue;
        const ts = Math.max(toMillis(l.time), last + 1000); // keep lines at least a second apart
        last = ts;
        await sendToDimensionLabs({
          text: l.content,
          userId,
          incoming: l.user_id == null, // lines from a Dialpad user are your side
          sessionId,
          conversationId: sessionId,
          dimensionlabs_timestamp: ts,
          platformJson: { direction: c.direction, groupId: c.group_id, masterCallId: c.master_call_id, labels: c.labels, csatScore: c.csat_score, moments },
        }, { historical: historical || ts < Date.now() - HOUR });
      }
      newest = Math.max(newest, ended);
    }
    cursor = page?.items?.length ? page.cursor : null;
  } while (cursor);
  return newest;
}
```

## 5. Run it

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

- **History first:** `node run.mjs 2025-01-01` sends every call since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends calls that ended since the last run to `/track`, and they are enriched automatically every day. Lines of a call longer than an hour go to `/trackhistorical`, and need a backfill.
- **Re-runs are safe:** duplicates are removed automatically.
- **Rate limits:** Dialpad allows 20 requests a second per company, and 1,200 a minute each for listing calls and getting transcripts. The code waits whenever Dialpad returns `429`.

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.

## Dialpad reference

- [Manual API Key Generation](https://developers.dialpad.com/docs/api-key-generation)
- [Call: List](https://developers.dialpad.com/reference/calllist)
- [Call Transcript: Get](https://developers.dialpad.com/reference/transcriptsget)
- [Rate Limits](https://developers.dialpad.com/docs/rate-limits)
- [Token Generation via OAuth](https://developers.dialpad.com/docs/oauth)

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