# Vonage Contact Center

> Send call transcripts from Vonage Contact Center to Dimension Labs with the universal API.

This brings in Vonage Contact Center call transcripts from Conversation Analyzer, one call as one message. One call becomes one conversation in Dimension Labs. You run it on your side: a small script pulls from the Vonage Contact Center (VCC) APIs and posts each transcript to the [universal API](/api/tracker).

The transcript the API returns is one block of text with no speakers, so each call is sent as a single message, as described in [Transcripts in one block](/integrations/transcripts#option-2-send-it-as-one-message). Dimensions are still extracted from the whole call. What you can't do is separate what the customer said from what your agent said.

## What you need

- Conversation Analyzer on your VCC account. It is a provisioned feature: if you don't have it, ask Vonage. Call recording must be on too.
- Calls set up for analysis. Vonage lists these settings:
  - In **User Admin > Agent Settings**, set **Transcribe calls** to **Yes** for each agent whose calls you want.
  - Your interaction plan sets a language and a Conversation Analyzer profile on each call. Without both, Conversation Analyzer doesn't analyze the call.
- API credentials with the `interaction-content:read` scope, created by a user who can see the calls: **View inbound interaction content**, **View outbound interaction content**, and access to the groups of the agents who took the calls. A token has the permissions of the user who created the credentials.
- 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 Vonage Contact Center

Vonage's steps are in [How to authenticate with a Vonage Contact Center (VCC) API](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3567190030).

1. Log in to the VCC Admin Portal and go to **API Credentials**. If you don't see it, contact VCC support.
2. Click **Create New**. Give the credentials a name, and under **Scopes** add `interaction-content:read`.
3. Click **Save API credential**, and copy the client ID and secret. You can't see the secret again.
4. Find your region: `emea`, `nam` (USA) or `apac`. The code takes it as `VONAGE_REGION`.
5. Get a token: `POST https://YOUR_REGION.cc.vonage.com/Auth/connect/token` with a form body of `grant_type=client_credentials`, `client_id`, `client_secret` and `scope=interaction-content:read`. Send it as `Authorization: Bearer YOUR_TOKEN`. Vonage says a token lasts 15 minutes by default, so the code gets a new one when a request returns `401`.

Every request also needs a version in the `Accept` header. The code uses `application/vnd.newvoicemedia.v3+json`.

## 2. Pull new transcripts

Find calls that have a transcript with the Interaction Content API, then get their transcripts from the Conversation Analyzer API, up to 50 calls at a time:

```bash
curl -H "Authorization: Bearer $VCC_TOKEN" -H "Accept: application/vnd.newvoicemedia.v3+json" \
  "https://nam.api.cc.vonage.com/interaction-content/interactions?start=2026-10-20T00:00:00Z&end=2026-10-26T00:00:00Z"

curl -H "Authorization: Bearer $VCC_TOKEN" -H "Accept: application/vnd.newvoicemedia.v3+json" \
  "https://nam.api.cc.vonage.com/conversation-analyzer/interactions?calls=GUID_1,GUID_2&transcript=true"
```

- `start` and `end` are ISO 8601, and a search can span at most 7 days. The code searches 6 days at a time, from `VONAGE_LOOKBACK_DAYS` before the cursor, and keeps calls whose transcript changed since the last run.
- The search only returns interactions that have at least one content file. Each item has `guid`, `start`, `direction`, `handlingAgents` and `content[]`. A call with a transcript has a content item with `type` `transcript`, and its `lastModified` is the code's cursor.
- `limit` sets the page size (1 to 1,000, 1,000 by default). When `meta.nextContinuationToken` is set, send it as `continuationToken` for the next page.
- `calls` takes up to 50 comma-separated GUIDs. With `transcript=true`, each item has `transcript`, a single string of the call's text with no speaker labels. It also has `status` (`processing`, `processed` or `failed`), `language`, `confidence`, `categories`, `callTimeBreakDown` (seconds of agent talk, customer talk, silence and cross talk) and, when sentiment is on, `sentimentSummary`.

**About speakers.** Conversation Analyzer separates the speakers on its own screen, but the API's `transcript` doesn't: Vonage's example is `"Case.\nHi, and yeah, just a call here. Thank you bye."`. The Interaction Content API can also return the transcript as a JSON file (`GET /interaction-content/interactions/{guid}/content/transcript`), but Vonage doesn't document that file's layout. If you download one and it does label each line with a speaker, you can split it into turns instead: see [Transcripts in one block](/integrations/transcripts).

## 3. Map the fields

| Dimension Labs | Vonage Contact Center | Notes |
|---|---|---|
| `text` | `transcript` | The whole call, with line breaks turned into spaces. Calls with an empty transcript are skipped. |
| `userId` | `guid` | The API has no customer id: `connectFrom` and `connectTo` are phone numbers. The code uses the `sessionId`, or look up your own internal customer id in your system. |
| `incoming` | none | Always `true`: the transcript holds the customer's words, and there is nothing to split it on. |
| `sessionId` | `guid` | One call is one conversation in Dimension Labs. |
| `conversationId` | `guid` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `start` | When the call started. |
| `platformJson` | `direction`, `language`, `confidence`, `callTimeBreakDown`, category names, `sentimentSummary.overallSentiment` | Marked `transcript: 'unsplit'` so you can filter these records. Leave out `connectFrom` and `connectTo`: they are phone numbers. |

**Personal data.** Call transcripts hold names, phone numbers, card and account numbers, and addresses. Vonage has a personal data redaction feature for call recordings and transcripts (a provisioned feature, for English language profiles); use it, or redact before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// vonage-contact-center.mjs. Environment: VONAGE_REGION (emea, nam or apac), VONAGE_CLIENT_ID, VONAGE_CLIENT_SECRET,
// VONAGE_LOOKBACK_DAYS (default 2), DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const WINDOW = 6 * DAY; // a search can span at most 7 days
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const REGION = process.env.VONAGE_REGION ?? 'nam';
const API = `https://${REGION}.api.cc.vonage.com`;
const LOOKBACK = Number(process.env.VONAGE_LOOKBACK_DAYS ?? 2) * DAY;
let token;

async function newToken() {
  const body = new URLSearchParams({
    grant_type: 'client_credentials', client_id: process.env.VONAGE_CLIENT_ID,
    client_secret: process.env.VONAGE_CLIENT_SECRET, scope: 'interaction-content:read',
  });
  const res = await fetch(`https://${REGION}.cc.vonage.com/Auth/connect/token`, { method: 'POST', body });
  if (!res.ok) throw new Error(`Vonage token request returned ${res.status}: ${await res.text()}`);
  token = (await res.json()).access_token;
}

// Gets a fresh token on 401, and retries rate limits (429) and server errors (5xx). Returns null on 404.
async function get(path) {
  for (let attempt = 0; ; attempt++) {
    if (!token) await newToken();
    const res = await fetch(`${API}${path}`, {
      headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.newvoicemedia.v3+json' },
    });
    if (res.ok) return res.json();
    if (res.status === 404) return null;
    if (res.status === 401 && attempt === 0) { token = null; continue; }
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Vonage returned ${res.status} for ${path}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Calls whose transcript changed between `since` and `until`, with that time as `changed`.
async function* callsWithNewTranscripts(since, until) {
  for (let from = since - LOOKBACK; from < until; from += WINDOW) {
    const to = Math.min(from + WINDOW, until);
    let continuationToken;
    do {
      const q = new URLSearchParams({ start: new Date(from).toISOString(), end: new Date(to).toISOString(), limit: '1000' });
      if (continuationToken) q.set('continuationToken', continuationToken);
      const page = await get(`/interaction-content/interactions?${q}`);
      for (const item of page?.items ?? []) {
        const t = (item.content ?? []).find((c) => c.type === 'transcript');
        const changed = t?.lastModified ? toMillis(t.lastModified) : null;
        if (changed !== null && changed >= since && changed < until) yield { ...item, changed };
      }
      continuationToken = page?.meta?.nextContinuationToken;
    } while (continuationToken);
  }
}

async function sendBatch(batch, historical) {
  let held = Infinity; // the earliest call still being processed: the cursor stays before it
  const found = await get(`/conversation-analyzer/interactions?calls=${batch.map((c) => c.guid).join(',')}&transcript=true`);
  const byGuid = new Map((Array.isArray(found) ? found : []).map((r) => [r.guid, r]));
  for (const call of batch) {
    const r = byGuid.get(call.guid);
    if (r?.status === 'processing') { held = Math.min(held, call.changed); continue; }
    const text = (r?.transcript ?? '').replace(/\s+/g, ' ').trim();
    if (r?.status !== 'processed' || !text) continue;
    const sessionId = `vonage-${call.guid}`.replace(/:/g, '-');
    const ts = toMillis(call.start);
    await sendToDimensionLabs({
      text,
      userId: sessionId, // no customer id in the API; connectFrom is a phone number
      incoming: true,    // it holds the customer's words
      sessionId,
      conversationId: sessionId,
      dimensionlabs_timestamp: ts,
      platformJson: {
        transcript: 'unsplit', channel: 'voice', direction: call.direction, language: r.language, confidence: r.confidence,
        callTimeBreakDown: r.callTimeBreakDown ?? r.callTimeBreakdown,
        categories: (r.categories ?? []).map((c) => c.category),
        sentiment: r.sentimentSummary?.overallSentiment,
      },
    }, { historical: historical || ts < Date.now() - HOUR });
  }
  return held;
}

// Sends every call transcript that appeared or changed at or after `since` (Unix ms). Returns the new `since`.
export async function syncVonage({ since, historical = false }) {
  token = null; // a new token for each run
  const until = Date.now();
  let newest = since;
  let held = Infinity;
  let batch = [];
  for await (const call of callsWithNewTranscripts(since, until)) {
    batch.push(call);
    newest = Math.max(newest, call.changed);
    if (batch.length === 50) { held = Math.min(held, await sendBatch(batch, historical)); batch = []; }
  }
  if (batch.length) held = Math.min(held, await sendBatch(batch, historical));
  return Math.min(newest, held);
}
```

A call that Conversation Analyzer is still processing keeps the cursor where it is, so the next run asks for it again. Calls already sent in between come back too, which is fine: duplicates are removed automatically.

This guide covers calls. Chat transcripts are content files too (`type` `chatTranscript`, fetched by their `contentKey`), but Vonage doesn't document their layout. If you want them, download one and check it against [Transcripts in one block](/integrations/transcripts).

## 5. Run it

Save the code as `vonage-contact-center.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 { syncVonage as sync } from './vonage-contact-center.mjs';`.

- **History first:** `node run.mjs 2025-01-01` sends every call transcript since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab. Only calls whose content your retention settings still keep have a transcript.
- **Then every 15 minutes:** the scheduled job sends new transcripts to `/track`, and they are enriched automatically every day. Conversation Analyzer processes a call after it ends, so a transcript can arrive more than an hour after the call started: those go to `/trackhistorical`, and need a backfill. The search looks back `VONAGE_LOOKBACK_DAYS` (2 by default) for calls whose transcript is new; raise it if your transcripts take longer.
- **Re-runs are safe:** duplicates are removed automatically.
- **Rate limits:** the code waits whenever Vonage returns `429`.

Vonage also sends an `interaction.transcriptavailable.v1` webhook event when a transcript is ready, if you'd rather react to events than poll. 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.

## Vonage Contact Center reference

- [How to authenticate with a Vonage Contact Center (VCC) API](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3567190030)
- [Search for interactions (Interaction Content API)](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3567190597)
- [Get single interaction content file](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3567779931)
- [Get multiple interactions (Conversation Analyzer API)](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3565192829)
- [Get a single interaction (Conversation Analyzer API)](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3566175010)
- [Versioning (Conversation Analyzer API)](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3567190128)
- [Configuring permissions for accessing interaction content](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/3748855817)
- [interaction.transcriptavailable.v1](https://docs-vcc.atlassian.net/wiki/spaces/VCCA/pages/5264506894)
- [Conversation Analyzer](https://docs-vcc.atlassian.net/wiki/spaces/DP/pages/666108166)
- [Personal data redaction in interaction recordings](https://docs-vcc.atlassian.net/wiki/spaces/DP/pages/5734465539)

Checked against Vonage Contact Center's documentation on 2026-10-26.
