Skip to content

02 Get your data in /Integrations

Genesys Cloud CX

Send messaging, email and call transcripts from Genesys Cloud CX to Dimension Labs with the universal API.

  1. GC Genesys Cloud CXTickets, 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 Genesys Cloud CX conversations, turn by turn: digital messaging (web messaging, SMS, WhatsApp, social and other message channels, bot turns included) and email first, and call transcripts when your licence includes voice transcription. One Genesys conversation becomes one conversation in Dimension Labs. You run it on your side: a small script pulls from the Genesys Cloud API and posts each message to the universal API.

  • A recording policy that keeps the interactions you want. Genesys stores the text of a message or email conversation as a recording, so a conversation that no policy recorded has no transcript to pull. Policies set a media type (Call, Chat, Email, Message) and a retention period, and they apply only to future interactions.
  • An OAuth client with the Client Credentials grant, and a role on it with these permissions:
    • analytics:conversationDetail:view, to find conversations.
    • recording:recording:view (or recording:recordingSegment:view), to read transcripts.
    • recording:recording:viewSensitiveData. Without it, and with sensitive data redaction on in your org, Genesys returns redacted text with no error. Decide on purpose whether you want Genesys’s redaction or your own (see Personal data below).
  • For call transcripts only: voice transcription turned on, and a licence that includes it. Genesys lists Genesys Cloud EX, or CX 1 or CX 2 with the Speech and Text Analytics add-on or equivalent WEM add-ons. Check yours with your Genesys account team. Call recording must be on as well.
  • A Dimension Labs API key for a new Universal API source. See Your API key.
  1. Go to Menu > IT and Integrations > OAuth and add a client. Genesys’s steps are in Create an OAuth client.
  2. Choose the Client Credentials grant type. Assign a role that holds the permissions above (you can only assign roles you hold yourself), with the divisions whose conversations you want. Save, and copy the client ID and client secret.
  3. Find your org’s region. Each region has its own hosts: login.mypurecloud.com and api.mypurecloud.com for US East, login.usw2.pure.cloud for US West, login.mypurecloud.ie for Ireland, login.mypurecloud.de for Frankfurt, and so on. The full list is in Platform API. The code takes the part after login. as GENESYS_REGION.
  4. Get a token: POST https://login.YOUR_REGION/oauth/token with the body grant_type=client_credentials and the header Authorization: Basic BASE64(client_id:client_secret). The response has access_token and expires_in. Send it as Authorization: Bearer YOUR_TOKEN.

For call transcripts, also turn on voice transcription: in Speech and Text Analytics settings, then on the queues (or with the Transcription action in your Architect call flows). Genesys’s steps are in Configure voice transcription.

Find conversations with the analytics conversation details query, then get each one’s transcripts:

Terminal window
curl -X POST "https://api.mypurecloud.com/api/v2/analytics/conversations/details/query" \
-H "Authorization: Bearer $GENESYS_TOKEN" -H "Content-Type: application/json" \
-d '{"interval":"2026-10-25T00:00:00.000Z/2026-10-26T00:00:00.000Z","order":"desc","orderBy":"conversationEnd",
"conversationFilters":[{"type":"and","predicates":[{"dimension":"conversationEnd","operator":"exists"}]}],
"paging":{"pageSize":100,"pageNumber":1}}'
curl -H "Authorization: Bearer $GENESYS_TOKEN" \
"https://api.mypurecloud.com/api/v2/conversations/CONVERSATION_ID/recordings?formatId=NONE"
  • interval selects conversations by the day they started. With a filter, it can span up to 32 days (7 days without one). The code queries one day at a time, from GENESYS_LOOKBACK_DAYS before the cursor, and keeps conversations that ended since the last run. A message or email conversation that stays open longer than the lookback is missed: raise it if yours do.
  • Pages hold up to 100 conversations (pageSize, pageNumber). Page number times page size can’t pass 100,000 in one query.
  • /recordings returns one entry per recording. Message text is in messagingTranscript[] (messageText), email in emailTranscript[] (textBody). transcript[] only holds web chat from the retired ACD web chat, so it is empty for messaging conversations, which is not an error. formatId=NONE skips audio transcoding. A 202 means the recording is still being prepared: the code waits and asks again.

Call transcripts take two steps. Ask for the transcript URLs of a call, then download each one:

Terminal window
curl -H "Authorization: Bearer $GENESYS_TOKEN" \
"https://api.mypurecloud.com/api/v2/speechandtextanalytics/conversations/CONVERSATION_ID/communications/COMMUNICATION_ID/transcripturls"
  • The communication id is the sessionId of a voice session in the conversation details (Genesys calls each session a communication). It is not in /recordings. The code asks for each voice session of the customer and skips any without a transcript.
  • The response’s urls[].url are pre-signed links that expire after 10 minutes. Download them without your token.
  • Each file has transcripts[].phrases[], each with text, startTimeMs (Unix ms) and participantPurpose: external is the customer, internal is the agent, IVR or ACD.
  • Voicemail transcripts have no API.
Dimension LabsGenesys Cloud CXNotes
textmessages: messagingTranscript[].messageText; email: emailTranscript[].textBody; calls: phrases[].textSkip empty text (attachments, stickers and events have none). Email replies can carry the quoted thread below them.
userIdcustomer participant’s externalContactIdGenesys’s own contact id, when the customer is a known external contact. Otherwise the code uses the sessionId.
incomingmessages: the participant’s purpose; email: the from address; calls: participantPurposeMessages: true when the sender’s participant (participantId, looked up in the conversation details) has the purpose customer. Agents and bot flows are false. Email: false when from.email is at one of your own domains (GENESYS_EMAIL_DOMAINS). Calls: true when external.
sessionIdconversationIdOne Genesys conversation is one conversation in Dimension Labs.
conversationIdconversationIdSame value as sessionId.
dimensionlabs_timestampmessages: timestamp; email: time; calls: startTimeMsTurns closer than a second apart are spaced to 1,000 ms.
platformJsonmedia type, participant purpose, transcript language, originatingDirection, divisionIdsLeave out from, to, ani, dnis and participant names: they are addresses, phone numbers and names.

Personal data. Transcripts hold names, phone numbers, account numbers and addresses. Either keep Genesys’s sensitive data redaction on and leave recording:recording:viewSensitiveData off the role, so the API returns redacted text, or grant it and redact before you send. Check a few transcripts first: the API doesn’t tell you which one you are getting. See PII redaction.

// genesys-cloud.mjs. Environment: GENESYS_REGION (for example mypurecloud.com or usw2.pure.cloud), GENESYS_CLIENT_ID,
// GENESYS_CLIENT_SECRET, GENESYS_EMAIL_DOMAINS (your own email domains, comma-separated; emails are skipped without it),
// GENESYS_VOICE (true to also send call transcripts), GENESYS_LOOKBACK_DAYS (default 7), GENESYS_DELAY_MINUTES (default 30), DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const DAY = 24 * HOUR;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const REGION = process.env.GENESYS_REGION ?? 'mypurecloud.com';
const API = `https://api.${REGION}`;
const VOICE = process.env.GENESYS_VOICE === 'true';
const LOOKBACK = Number(process.env.GENESYS_LOOKBACK_DAYS ?? 7) * DAY;
const DELAY = Number(process.env.GENESYS_DELAY_MINUTES ?? 30) * 60 * 1000;
const OUR_DOMAINS = (process.env.GENESYS_EMAIL_DOMAINS ?? '').toLowerCase().split(',').map((d) => d.trim()).filter(Boolean);
let token;
async function newToken() {
const basic = Buffer.from(`${process.env.GENESYS_CLIENT_ID}:${process.env.GENESYS_CLIENT_SECRET}`).toString('base64');
const res = await fetch(`https://login.${REGION}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${basic}` },
body: 'grant_type=client_credentials',
});
if (!res.ok) throw new Error(`Genesys token request returned ${res.status}: ${await res.text()}`);
token = (await res.json()).access_token;
}
// Gets a fresh token on 401, waits while a recording is being prepared (202), and retries rate limits (429)
// and server errors (5xx). Returns null on 404.
async function call(path, body) {
for (let attempt = 0; ; attempt++) {
if (!token) await newToken();
const res = await fetch(`${API}${path}`, {
method: body ? 'POST' : 'GET',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: body && JSON.stringify(body),
});
if (res.status === 200) return res.json();
if (res.status === 404) return null;
if (res.status === 401 && attempt === 0) { token = null; continue; }
const retry = res.status === 202 || res.status === 429 || res.status >= 500;
if (!retry || attempt === 5) throw new Error(`Genesys returned ${res.status} for ${path}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
// Conversations that ended between `since` and `until`, from the analytics conversation details query.
// The interval selects conversations by the day they started, so it starts LOOKBACK before `since`.
async function* endedConversations(since, until) {
for (let day = Math.floor((since - LOOKBACK) / DAY) * DAY; day < until; day += DAY) {
const interval = `${new Date(day).toISOString()}/${new Date(Math.min(day + DAY, Date.now())).toISOString()}`;
for (let pageNumber = 1; ; pageNumber++) {
const page = await call('/api/v2/analytics/conversations/details/query', {
interval,
order: 'desc',
orderBy: 'conversationEnd',
conversationFilters: [{ type: 'and', predicates: [{ dimension: 'conversationEnd', operator: 'exists' }] }],
paging: { pageSize: 100, pageNumber },
});
const list = page?.conversations ?? [];
for (const c of list) {
const end = toMillis(c.conversationEnd);
if (end >= since && end < until) yield c;
}
if (list.length < 100 || toMillis(list[list.length - 1].conversationEnd) < since) break;
}
}
}
const ours = (address) => OUR_DOMAINS.includes(String(address ?? '').split('@').pop().toLowerCase());
// Every turn of one conversation, from its recordings (digital) and, with GENESYS_VOICE, its call transcripts.
async function turnsOf(c) {
const turns = [];
const purposeOf = Object.fromEntries((c.participants ?? []).map((p) => [p.participantId, p.purpose]));
const sessions = (c.participants ?? []).flatMap((p) => (p.sessions ?? []).map((s) => ({ ...s, purpose: p.purpose })));
const media = new Set(sessions.map((s) => s.mediaType));
if (media.has('message') || media.has('email') || media.has('chat')) {
const recordings = await call(`/api/v2/conversations/${c.conversationId}/recordings?formatId=NONE`);
for (const rec of recordings ?? []) {
for (const m of rec.messagingTranscript ?? []) {
const purpose = purposeOf[m.participantId] ?? m.purpose;
turns.push({ text: m.messageText, at: toMillis(m.timestamp), incoming: purpose === 'customer', media: 'message', purpose });
}
if (OUR_DOMAINS.length) {
for (const e of rec.emailTranscript ?? []) {
turns.push({ text: e.textBody, at: toMillis(e.time), incoming: !ours(e.from?.email), media: 'email' });
}
}
for (const m of rec.transcript ?? []) { // web chat from the retired ACD web chat
if (m.bodyType && m.bodyType !== 'STANDARD') continue;
turns.push({ text: m.body, at: toMillis(m.utc), incoming: m.participantPurpose === 'customer', media: 'chat', purpose: m.participantPurpose });
}
}
}
if (VOICE && media.has('voice')) {
for (const s of sessions.filter((x) => x.mediaType === 'voice' && x.purpose === 'customer')) {
const found = await call(`/api/v2/speechandtextanalytics/conversations/${c.conversationId}/communications/${s.sessionId}/transcripturls`);
for (const { url } of found?.urls ?? []) {
const res = await fetch(url); // a pre-signed URL: no Authorization header
if (!res.ok) throw new Error(`Transcript download returned ${res.status}`);
const file = await res.json();
for (const t of file.transcripts ?? []) {
for (const p of t.phrases ?? []) {
turns.push({ text: p.text, at: p.startTimeMs, incoming: p.participantPurpose === 'external', media: 'voice', language: t.language });
}
}
}
}
}
return turns.filter((t) => t.text?.trim()).sort((a, b) => a.at - b.at);
}
// Sends every conversation that ended at or after `since` (Unix ms). Returns the new `since`.
export async function syncGenesys({ since, historical = false }) {
token = null; // a new token for each run
const until = Date.now() - DELAY; // give recordings and transcripts time to appear
let newest = since;
for await (const c of endedConversations(since, until)) {
const sessionId = `genesys-${c.conversationId}`;
const customer = (c.participants ?? []).find((p) => p.purpose === 'customer');
const userId = customer?.externalContactId ? `genesys-${customer.externalContactId}` : sessionId;
let last = 0;
for (const t of await turnsOf(c)) {
const ts = Math.max(t.at, last + 1000); // keep turns at least a second apart
last = ts;
await sendToDimensionLabs({
text: t.text,
userId,
incoming: t.incoming,
sessionId,
conversationId: sessionId,
dimensionlabs_timestamp: ts,
platformJson: { media: t.media, purpose: t.purpose, language: t.language, originatingDirection: c.originatingDirection, divisionIds: c.divisionIds },
}, { historical: historical || ts < Date.now() - HOUR });
}
newest = Math.max(newest, toMillis(c.conversationEnd));
}
return newest;
}

Email is off until you set GENESYS_EMAIL_DOMAINS, because Genesys gives an email’s sender only as an address: the code compares it with your domains on your side and never sends it. Call transcripts are off until you set GENESYS_VOICE=true.

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

  • History first: node run.mjs 2025-01-01 sends every conversation that ended since that date to /trackhistorical. Then start a backfill from the Backfill tab. Only conversations a recording policy kept, and that are still inside your retention period, have text.
  • Then every 15 minutes: the scheduled job sends new conversations to /track, and they are enriched automatically every day. The code waits GENESYS_DELAY_MINUTES (30 by default) after a conversation ends, so its recordings and transcripts are ready, and sends it whole. Turns more than an hour old by then go to /trackhistorical, and need a backfill. If call transcripts show up later than that in your org, raise the delay.
  • Re-runs are safe: duplicates are removed automatically.
  • Rate limits: Genesys allows a client credentials token 400 requests a minute, and 120 recording requests a minute. The code waits whenever Genesys returns 429.

You can also upload a file and check it first with the file checker, then run a backfill.

Checked against Genesys Cloud CX’s documentation on 2026-10-26.