Skip to content

02 Get your data in /Integrations

Gong

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

  1. G GongTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

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.

  • 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.
  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.

Authenticate with Basic auth: the access key and secret joined with a colon, Base64-encoded (manage API keys).

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

Terminal window
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 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 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).

Dimension LabsGongNotes
textmonologue sentences[].textOne message per monologue: the sentences joined.
userIdparty userId, else a hash of emailAddress, else speakerIdMatch 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.
incomingparty affiliationtrue 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.
sessionIdcallIdPrefixed, for example gong-7782342274025937895.
conversationIdcallIdSame value as sessionId.
dimensionlabs_timestampcall started plus the first sentence’s startstart is milliseconds from the start of the call. Turns closer than a second apart are spaced to 1,000 ms.
platformJsondirection, system, scope, media, language, workspaceId, duration; monologue topicscope 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.

// 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.
import { createHash } from 'node:crypto';
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
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;
}

Save the code as gong.mjs in a project set up as in Set up your project, and run it with the 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 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 shares your Gong data to Snowflake, BigQuery or Databricks, refreshed daily; from there, follow the Snowflake or BigQuery guide. You can also upload a file of transcripts (see Call transcripts, and check it with the file checker), then run a backfill.

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