# Gong

> Send Gong call transcripts to Dimension Labs with the universal API, your team and the customer kept apart.

This brings in each Gong call as one conversation, turn by turn: what your prospects and customers said, and what your own team said back. You run it on your side: a small script pulls from the Gong API and posts each turn to the [universal API](/api/tracker).

## What you need

- A Gong tech admin: only they can create API keys. Gong's API is available on any Gong plan.
- A Gong API access key and secret, and your company's Gong API base URL.
- The email domains your company uses, for example `acme.com`, as a fallback for speakers Gong can't place.
- 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 Gong

1. From the left sidebar, click **Admin center**.
2. In the **Settings** tab, click **API** under **Ecosystem**.
3. Click **+ Get API key** and enter a name. **TTL (days)** and **Trusted IPs** are optional; if you set trusted IPs, the key only works from those addresses.
4. Click **Get API key**, then copy the access key and secret. You can't see the secret again after you close the dialog.
5. Find your base URL for API calls on Gong's API authentication page, as described in Gong's [API introduction](https://help.gong.io/apidocs/introduction-2).

Authenticate with Basic auth: the access key and secret joined with a colon, Base64-encoded ([manage API keys](https://help.gong.io/docs/receive-access-to-the-api)).

## 2. Pull new calls

Two `POST` endpoints take the same date filter, on call start time:

```bash
curl -u "$GONG_ACCESS_KEY:$GONG_ACCESS_KEY_SECRET" -H "Content-Type: application/json" \
  -d '{"filter":{"fromDateTime":"2026-10-25T00:00:00Z","toDateTime":"2026-10-26T00:00:00Z"},"contentSelector":{"exposedFields":{"parties":true}}}' \
  "$GONG_BASE_URL/v2/calls/extensive"

curl -u "$GONG_ACCESS_KEY:$GONG_ACCESS_KEY_SECRET" -H "Content-Type: application/json" \
  -d '{"filter":{"fromDateTime":"2026-10-25T00:00:00Z","toDateTime":"2026-10-26T00:00:00Z"}}' \
  "$GONG_BASE_URL/v2/calls/transcript"
```

- [`/v2/calls/extensive`](https://help.gong.io/apidocs/retrieve-detailed-call-data-by-various-filters-v2callsextensive-2) returns each call's metadata and, with `parties: true`, its participants. It needs the scope `api:calls:read:extensive` for OAuth apps.
- [`/v2/calls/transcript`](https://help.gong.io/apidocs/retrieve-transcripts-of-calls-by-date-or-callids-v2callstranscript-2) returns each call's transcript as monologues: a `speakerId`, a `topic` and the `sentences`, each with `start` and `end` in milliseconds from the start of the call. It needs `api:calls:read:transcript`.
- Both return a `records.cursor` when there are more pages. Repeat the call with that `cursor` and the same filter until it is missing. A `404` means no calls in the period.
- The filter is on when the call started, not when its transcript was ready. The code below waits `GONG_DELAY_HOURS` (3 by default) before it reads a call, so the transcript is there. Gong doesn't publish how long transcription takes: if recent calls arrive without their turns, raise the delay.

Rate limits, per Gong: 3 API calls per second and 10,000 per day. Going over returns `429` with a `Retry-After` header in seconds ([what the Gong API provides](https://help.gong.io/docs/what-the-gong-api-provides)).

## 3. Map the fields

| Dimension Labs | Gong | Notes |
|---|---|---|
| `text` | monologue `sentences[].text` | One message per monologue: the sentences joined. |
| `userId` | party `userId`, else a hash of `emailAddress`, else `speakerId` | Match the monologue's `speakerId` to the party with the same `speakerId`. Team members have a Gong `userId`; for others the code sends a hash of the email, never the email itself. |
| `incoming` | party `affiliation` | `true` for `External` (prospects, customers), `false` for `Internal` (your team). For `Unknown`, the code checks the email domain against `COMPANY_DOMAINS`: a match is your team, anything else is incoming. |
| `sessionId` | `callId` | Prefixed, for example `gong-7782342274025937895`. |
| `conversationId` | `callId` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | call `started` plus the first sentence's `start` | `start` is milliseconds from the start of the call. Turns closer than a second apart are spaced to 1,000 ms. |
| `platformJson` | `direction`, `system`, `scope`, `media`, `language`, `workspaceId`, `duration`; monologue `topic` | `scope` says whether the call had any external participants. |

Gong tells your team apart from everyone else itself: each party has an `affiliation` of `Internal`, `External` or `Unknown`, and the call has a `scope` of `Internal`, `External` or `Unknown`. Internal-only calls come through too; filter them later on `scope` if you only want customer calls.

**Personal data.** Speaker names, email addresses, phone numbers and job titles are in `parties`: don't send them, and don't send the call `title`, which often names the customer. The code uses Gong's user id or a hash instead. Transcripts themselves hold names, companies and numbers: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// gong.mjs. Environment: GONG_BASE_URL, GONG_ACCESS_KEY, GONG_ACCESS_KEY_SECRET, COMPANY_DOMAINS (for example "acme.com,acme.co.uk"),
// GONG_DELAY_HOURS (default 3), DL_API_KEY.



const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = process.env.GONG_BASE_URL.replace(/\/$/, '');
const auth = 'Basic ' + Buffer.from(`${process.env.GONG_ACCESS_KEY}:${process.env.GONG_ACCESS_KEY_SECRET}`).toString('base64');
const DELAY = Number(process.env.GONG_DELAY_HOURS ?? 3) * HOUR;
const DOMAINS = (process.env.COMPANY_DOMAINS ?? '').toLowerCase().split(',').map((d) => d.trim()).filter(Boolean);
const hash = (s) => createHash('sha256').update(s.trim().toLowerCase()).digest('hex').slice(0, 32);

// Retries Gong's rate limits (429) and server errors (5xx), honoring Retry-After. A 404 means no calls in the period.
async function post(path, body) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${base}${path}`, { method: 'POST', headers: { Authorization: auth, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
    if (res.ok) return res.json();
    if (res.status === 404) return {};
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Gong returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Reads every page, passing records.cursor back with the same filter.
async function all(path, body, key) {
  const items = [];
  for (let cursor; ; ) {
    const page = await post(path, cursor ? { ...body, cursor } : body);
    items.push(...(page[key] ?? []));
    cursor = page.records?.cursor;
    if (!cursor) return items;
  }
}

// Your team is outgoing. Gong's affiliation decides; for Unknown, fall back to your email domains.
function isInternal(party) {
  if (party?.affiliation === 'Internal') return true;
  if (party?.affiliation === 'External') return false;
  const domain = party?.emailAddress?.split('@')[1]?.toLowerCase();
  return Boolean(domain && DOMAINS.includes(domain));
}

// Sends every call that started at or after `since` (Unix ms) and at least GONG_DELAY_HOURS ago. Returns the new `since`.
export async function syncGong({ since, historical = false }) {
  const until = Date.now() - DELAY;
  if (until <= since) return since;
  const filter = { fromDateTime: new Date(since).toISOString(), toDateTime: new Date(until).toISOString() };
  const calls = new Map((await all('/v2/calls/extensive', { filter, contentSelector: { exposedFields: { parties: true } } }, 'calls'))
    .map((c) => [c.metaData.id, c]));
  for (const t of await all('/v2/calls/transcript', { filter }, 'callTranscripts')) {
    const call = calls.get(t.callId);
    if (!call) continue;
    const m = call.metaData;
    const speakers = new Map((call.parties ?? []).filter((p) => p.speakerId).map((p) => [p.speakerId, p]));
    const started = toMillis(m.started);
    let last = 0;
    for (const mono of t.transcript ?? []) {
      const text = (mono.sentences ?? []).map((s) => s.text).join(' ').trim();
      if (!text) continue;
      const party = speakers.get(mono.speakerId);
      const ts = Math.max(started + (mono.sentences[0].start ?? 0), last + 1000); // keep turns at least a second apart
      last = ts;
      await sendToDimensionLabs({
        text,
        userId: `gong-${party?.userId ?? (party?.emailAddress ? hash(party.emailAddress) : mono.speakerId)}`,
        incoming: !isInternal(party),
        sessionId: `gong-${t.callId}`,
        conversationId: `gong-${t.callId}`,
        dimensionlabs_timestamp: ts,
        platformJson: {
          callId: t.callId, topic: mono.topic, affiliation: party?.affiliation ?? 'Unknown', direction: m.direction, system: m.system,
          scope: m.scope, media: m.media, language: m.language, workspaceId: m.workspaceId, durationSeconds: m.duration,
        },
      }, { historical: historical || ts < Date.now() - HOUR });
    }
  }
  return until;
}
```

## 5. Run it

Save the code as `gong.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 { syncGong as sync } from './gong.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 once they are `GONG_DELAY_HOURS` old. By then every turn is more than an hour old, so it goes to `/trackhistorical`: start a backfill from the **Backfill** tab whenever you want the latest calls enriched.
- **Re-runs are safe:** duplicates are removed automatically. If a call's transcript was late, run `node run.mjs` with a date before it to pick it up.

Gong allows 10,000 API calls a day, shared by every integration on your Gong account. Each run costs a few calls per page of results, so a history load fits comfortably; the code waits whenever Gong asks it to. For analytics at scale, Gong's [data cloud](https://help.gong.io/docs/choosing-between-gongs-api-and-data-cloud-for-analytics) shares your Gong data to Snowflake, BigQuery or Databricks, refreshed daily; from there, follow the [Snowflake](/integrations/snowflake) or [BigQuery](/integrations/bigquery) guide. You can also [upload a file](/data/upload-a-file) of transcripts (see [Call transcripts](/transform/recipe-call-transcripts), and check it with the [file checker](/data/check-a-file)), then run a backfill.

## Gong reference

- [Manage API keys](https://help.gong.io/docs/receive-access-to-the-api)
- [Gong API introduction (base URL, authentication, limits, cursors)](https://help.gong.io/apidocs/introduction-2)
- [What the Gong API provides](https://help.gong.io/docs/what-the-gong-api-provides)
- [Retrieve transcripts of calls (/v2/calls/transcript)](https://help.gong.io/apidocs/retrieve-transcripts-of-calls-by-date-or-callids-v2callstranscript-2)
- [Retrieve detailed call data (/v2/calls/extensive)](https://help.gong.io/apidocs/retrieve-detailed-call-data-by-various-filters-v2callsextensive-2)
- [Choosing between Gong's API and data cloud](https://help.gong.io/docs/choosing-between-gongs-api-and-data-cloud-for-analytics)

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