Skip to content

02 Get your data in /Integrations

RingCentral

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

  1. R RingCentralTickets, 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 transcripts of recorded RingCentral RingEX calls, turn by turn, from AI Conversation Expert (ACE, formerly RingSense). One call becomes one conversation in Dimension Labs. You run it on your side: a small script pulls from the RingCentral API and posts each message to the universal API.

  • An AI Conversation Expert licence. ACE is a paid add-on: the licence has to be bought and assigned to the users whose calls you want. RingCentral only transcribes calls that are recorded by a user with an active ACE licence, and keeps ACE data for 1 year by default (set in your ACE settings).
  • Access to the AI Conversation Expert API, which is in beta. Your app needs the RingSense app scope. If the Developer Console doesn’t offer it, request it from RingCentral. RingCentral’s developer team says ACE is the only API for call transcripts.
  • Call recording on the calls you want, automatic or on demand. RingCentral doesn’t keep recordings shorter than 30 seconds.
  • A RingCentral user for the sync whose role has the AI Conversation Expert - Access Insights permission (to read ACE data for every licensed user) and can read the company call log.
  • A Dimension Labs API key for a new Universal API source. See Your API key.

This covers RingEX voice calls. RingCentral has no API to find the recordings of RingCX calls, so their insights only arrive through ACE event notifications, which this script doesn’t use.

  1. In the RingCentral Developer Console, go to Apps and select Create App. Choose REST API App, then under Authentication select JWT auth flow. RingCentral’s steps are in JWT quick start.
  2. Under Security, add the ReadCallLog and RingSense permissions, and make the app private to your account. Note the client ID and client secret.
  3. Signed in as the sync user, open Credentials (under your name in the top right) and select Create JWT. RingCentral’s steps are in Create a JWT credential.
  4. Get a token: POST https://platform.ringcentral.com/restapi/oauth/token with the header Authorization: Basic BASE64(client_id:client_secret) and the body grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=YOUR_JWT. Send the access_token from the response as Authorization: Bearer YOUR_TOKEN.

To check the sync user’s permission, send GET /restapi/v1.0/account/~/extension/~/authz-profile/check?permissionId=ReadRingSenseInsights: "successful": true means it can read ACE data.

Find recorded calls in the company call log, then get each recording’s ACE insights:

Terminal window
curl -H "Authorization: Bearer $RC_TOKEN" \
"https://platform.ringcentral.com/restapi/v1.0/account/~/call-log?recordingType=All&dateFrom=2026-10-25T00:00:00.000Z&dateTo=2026-10-26T00:00:00.000Z&perPage=100"
curl -H "Authorization: Bearer $RC_TOKEN" \
"https://platform.ringcentral.com/ai/ringsense/v1/public/accounts/~/domains/pbx/records/RECORDING_ID/insights"
  • recordingType=All returns only calls with a recording. dateFrom and dateTo filter by the call’s start time, in ISO 8601 with a time zone. The code looks back RC_LOOKBACK_HOURS (24 by default) before the cursor and keeps calls that ended since the last run (startTime plus duration).
  • Follow navigation.nextPage.uri for the next page.
  • Each record’s recording.id is the sourceRecordId ACE needs. A call transferred between agents can have a recording per leg, all under one telephonySessionId.
  • The insights response has recordingStartTime, speakerInfo[] and insights.Transcript[], each turn with text, start and end (seconds into the recording) and speakerId. Look the speaker up in speakerInfo: a speaker with an extensionId is a user on your account; a call party outside the account has extensionId null.
  • The response also holds Summary, NextSteps, Sentiment and other insights. The code keeps the overall Sentiment.
Dimension LabsRingCentralNotes
textinsights.Transcript[].textSkip empty turns.
userIdsourceSessionIdACE gives no id for the outside party, only a name. The code uses the sessionId.
incomingspeakerInfo[].extensionIdfalse when the turn’s speaker has an extensionId (a user on your account). true otherwise.
sessionIdsourceSessionIdThe telephony session id: all recorded legs of one call share it.
conversationIdsourceSessionIdSame value as sessionId.
dimensionlabs_timestamprecordingStartTime plus Transcript[].startstart is seconds into the recording. Turns closer than a second apart are spaced to 1,000 ms.
platformJsonsourceRecordId, callDirection, domain, insights.SentimentLeave out title and speakerInfo[].name: they hold the caller’s and agent’s names. Leave out the call log’s from and to: they are phone numbers and names.

Personal data. Transcripts hold what people say: names, phone numbers, card and account numbers. ACE names the speakers and builds the call’s title from their names, so the code never sends either. Redact the text before you send. See PII redaction.

// ringcentral.mjs. Environment: RC_CLIENT_ID, RC_CLIENT_SECRET, RC_JWT, RC_SERVER (default https://platform.ringcentral.com),
// RC_LOOKBACK_HOURS (default 24), RC_DELAY_MINUTES (default 60), DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const SERVER = process.env.RC_SERVER ?? 'https://platform.ringcentral.com';
const LOOKBACK = Number(process.env.RC_LOOKBACK_HOURS ?? 24) * HOUR;
const DELAY = Number(process.env.RC_DELAY_MINUTES ?? 60) * 60 * 1000;
let token;
async function newToken() {
const basic = Buffer.from(`${process.env.RC_CLIENT_ID}:${process.env.RC_CLIENT_SECRET}`).toString('base64');
const res = await fetch(`${SERVER}/restapi/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${basic}` },
body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: process.env.RC_JWT }),
});
if (!res.ok) throw new Error(`RingCentral 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 (no insights for that recording).
async function get(url) {
for (let attempt = 0; ; attempt++) {
if (!token) await newToken();
const res = await fetch(url.startsWith('http') ? url : `${SERVER}${url}`, { headers: { Authorization: `Bearer ${token}` } });
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(`RingCentral returned ${res.status} for ${url}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || (res.status === 429 ? 30 : 2 ** attempt)) * 1000);
}
}
// Recorded calls that started in [from, to), from the company call log.
async function* recordedCalls(from, to) {
const q = new URLSearchParams({ recordingType: 'All', dateFrom: new Date(from).toISOString(), dateTo: new Date(to).toISOString(), perPage: '100' });
let url = `/restapi/v1.0/account/~/call-log?${q}`;
while (url) {
const page = await get(url);
yield* page?.records ?? [];
url = page?.navigation?.nextPage?.uri ?? null;
}
}
// Sends every recorded call that ended at or after `since` (Unix ms). Returns the new `since`.
export async function syncRingCentral({ since, historical = false }) {
token = null; // a new token for each run
const until = Date.now() - DELAY; // give AI Conversation Expert time to analyze the recording
if (until <= since) return since;
const seen = new Set();
for await (const c of recordedCalls(since - LOOKBACK, until)) {
const recordingId = c.recording?.id;
if (!recordingId || seen.has(recordingId)) continue;
seen.add(recordingId);
const ended = toMillis(c.startTime) + (c.duration ?? 0) * 1000;
if (ended < since || ended >= until) continue;
const ace = await get(`/ai/ringsense/v1/public/accounts/~/domains/pbx/records/${recordingId}/insights`);
if (!ace?.insights?.Transcript?.length) continue;
// A speaker with an extensionId is a user on your account (your side); anyone else is the customer.
const ours = new Set((ace.speakerInfo ?? []).filter((s) => s.extensionId).map((s) => s.speakerId));
const sessionId = `ringcentral-${ace.sourceSessionId ?? c.telephonySessionId ?? c.sessionId}`;
const start = toMillis(ace.recordingStartTime);
let last = 0;
for (const t of ace.insights.Transcript) {
if (!t.text?.trim()) continue;
const ts = Math.max(start + Math.round((t.start ?? 0) * 1000), last + 1000); // keep turns at least a second apart
last = ts;
await sendToDimensionLabs({
text: t.text,
userId: sessionId,
incoming: !ours.has(t.speakerId),
sessionId,
conversationId: sessionId,
dimensionlabs_timestamp: ts,
platformJson: { recordingId, direction: ace.callDirection, domain: ace.domain, sentiment: ace.insights.Sentiment?.[0]?.value },
}, { historical: historical || ts < Date.now() - HOUR });
}
}
return until;
}

Save the code as ringcentral.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncRingCentral as sync } from './ringcentral.mjs';.

  • History first: node run.mjs 2026-01-01 sends every recorded call since that date to /trackhistorical. Then start a backfill from the Backfill tab. Only calls ACE analyzed, and still inside your ACE retention period, have a transcript.
  • Then every 15 minutes: the scheduled job sends calls that ended since the last run to /track, and they are enriched automatically every day. ACE analyzes a call after the recording ends, so the code waits RC_DELAY_MINUTES (60 by default). A call whose insights aren’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: RingCentral limits requests per user per minute by group, and the company call log is in the Heavy group (10 requests a minute by default). Check your app’s limits under Rate Limits in the Developer Console. The code waits whenever RingCentral returns 429, for Retry-After seconds, or 30 seconds without it.

Without an ACE licence there is no transcript to pull through the API. You can still upload a file of transcripts you have, check it first with the file checker, then run a backfill.

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