02 Get your data in /Integrations
RingCentral
Send call transcripts from RingCentral to Dimension Labs with the universal API.
- R RingCentralTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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
RingSenseapp 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. Get API access in RingCentral
Section titled “1. Get API access in RingCentral”- 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.
- Under Security, add the
ReadCallLogandRingSensepermissions, and make the app private to your account. Note the client ID and client secret. - 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.
- Get a token:
POST https://platform.ringcentral.com/restapi/oauth/tokenwith the headerAuthorization: Basic BASE64(client_id:client_secret)and the bodygrant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=YOUR_JWT. Send theaccess_tokenfrom the response asAuthorization: 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.
2. Pull new calls
Section titled “2. Pull new calls”Find recorded calls in the company call log, then get each recording’s ACE insights:
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=Allreturns only calls with a recording.dateFromanddateTofilter by the call’s start time, in ISO 8601 with a time zone. The code looks backRC_LOOKBACK_HOURS(24 by default) before the cursor and keeps calls that ended since the last run (startTimeplusduration).- Follow
navigation.nextPage.urifor the next page. - Each record’s
recording.idis thesourceRecordIdACE needs. A call transferred between agents can have a recording per leg, all under onetelephonySessionId. - The insights response has
recordingStartTime,speakerInfo[]andinsights.Transcript[], each turn withtext,startandend(seconds into the recording) andspeakerId. Look the speaker up inspeakerInfo: a speaker with anextensionIdis a user on your account; a call party outside the account hasextensionIdnull. - The response also holds
Summary,NextSteps,Sentimentand other insights. The code keeps the overallSentiment.
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | RingCentral | Notes |
|---|---|---|
text | insights.Transcript[].text | Skip empty turns. |
userId | sourceSessionId | ACE gives no id for the outside party, only a name. The code uses the sessionId. |
incoming | speakerInfo[].extensionId | false when the turn’s speaker has an extensionId (a user on your account). true otherwise. |
sessionId | sourceSessionId | The telephony session id: all recorded legs of one call share it. |
conversationId | sourceSessionId | Same value as sessionId. |
dimensionlabs_timestamp | recordingStartTime plus Transcript[].start | start is seconds into the recording. Turns closer than a second apart are spaced to 1,000 ms. |
platformJson | sourceRecordId, callDirection, domain, insights.Sentiment | Leave 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.
4. Send it
Section titled “4. Send it”// 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;}5. Run it
Section titled “5. Run it”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-01sends 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 waitsRC_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, forRetry-Afterseconds, 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.
RingCentral reference
Section titled “RingCentral reference”- AI Conversation Expert API
- AI Conversation Expert (ACE) developer guide
- Call recordings
- Call log access control
- JWT quick start
- JWT flow
- Create a JWT credential
- Rate limits
- Developer community: transcriptions and API access
Checked against RingCentral’s documentation on 2026-10-26.