# Google Customer Experience Insights

> Send call and chat transcripts from Google Customer Experience Insights to Dimension Labs with the universal API.

This brings in Customer Experience Insights (CX Insights, formerly Contact Center AI Insights) conversations, segment by segment: call transcripts and chat transcripts, with their per-segment sentiment. One CX Insights conversation becomes one conversation in Dimension Labs. You run it on your side: a small script pulls from the Contact Center AI Insights API and posts each message to the [universal API](/api/tracker). For a large history in one go, you can also [export to BigQuery](#export-to-bigquery) instead.

## What you need

- Conversations already in CX Insights, with transcripts. Google now documents the product under Gemini Enterprise for Customer Experience; the API is still the Contact Center AI Insights API (`contactcenterinsights.googleapis.com`).
- A service account in the project that holds them, with a role that includes `contactcenterinsights.conversations.list`.
- 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 Google Cloud

1. Create a service account for the job, and grant it a role with `contactcenterinsights.conversations.list` on the project. Creating the account works as in the [BigQuery guide](/integrations/bigquery).
2. Run the job with that service account as its identity. The code gets its token through Application Default Credentials with the `https://www.googleapis.com/auth/cloud-platform` scope. For local testing, `gcloud auth application-default login` uses your own account.
3. Note your project ID and location. Google's examples use `us-central1`, the global instance; if your conversations sit in a regional instance, use that location (see [Regionalization](https://docs.cloud.google.com/gemini-enterprise-cx/insights/docs/regionalization)).

## 2. Pull new conversations

List conversations changed since your cursor, with the full view:

```bash
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  "https://contactcenterinsights.googleapis.com/v1/projects/PROJECT_ID/locations/us-central1/conversations?view=FULL&pageSize=100&filter=update_time>\"2026-10-25T00:00:00Z\""
```

- `view=FULL` is required. The list defaults to `BASIC`, which leaves out the transcript.
- `filter` takes the terms on [Filter conversations](https://docs.cloud.google.com/gemini-enterprise-cx/insights/filtering), for example `update_time`, `create_time`, `start_time`, `medium="PHONE_CALL"` or `labels.key:"value"`. The code uses `update_time`, so a conversation whose transcript or analysis changes is sent again (the duplicates are removed).
- `pageSize` defaults to 100. When the response has `nextPageToken`, send it as `pageToken` for the next page.
- Each conversation has `transcript.transcriptSegments[]`, each with `text`, `segmentParticipant.role`, `channelTag`, `messageTime` (when provided), `words[].startOffset` and `sentiment`.

## 3. Map the fields

| Dimension Labs | CX Insights | Notes |
|---|---|---|
| `text` | `transcriptSegments[].text` | Skip empty segments. |
| `userId` | `obfuscatedUserId` | The obfuscated user ID your integration sent. If it is empty, the code uses the `sessionId`. |
| `incoming` | `segmentParticipant.role`, or `channelTag` | `true` when the role is `END_USER`. `HUMAN_AGENT`, `AUTOMATED_AGENT` and `ANY_AGENT` are your side. Calls without roles: `true` when `channelTag` equals `callMetadata.customerChannel`. A segment with neither is treated as your side, so set roles or channels when you ingest. |
| `sessionId` | conversation id | The last part of `name`. |
| `conversationId` | conversation id | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | `messageTime`, or `startTime` plus the first word's `startOffset` | `messageTime` is set when your integration provided it. Otherwise the code adds the first word's offset (from the start of the conversation) to `startTime`. Segments closer than a second apart are spaced to 1,000 ms. |
| `platformJson` | `medium`, `languageCode`, `agentId`, `labels`, segment `role` and `sentiment.score` | `agentId` is the opaque id you gave the agent at ingest. |

**Personal data.** Transcripts hold names, phone numbers, card and account numbers. CX Insights can redact transcripts with Sensitive Data Protection when you ingest them; otherwise redact before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

Install Google's auth library in your project first: `npm install google-auth-library`.

```js
// google-cx-insights.mjs. Environment: CXI_PROJECT (project ID), CXI_LOCATION (for example us-central1), DL_API_KEY.
// Google credentials come from Application Default Credentials (a service account), as in the BigQuery guide.



const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const auth = new GoogleAuth({ scopes: 'https://www.googleapis.com/auth/cloud-platform' });
const BASE = `https://contactcenterinsights.googleapis.com/v1/projects/${process.env.CXI_PROJECT}/locations/${process.env.CXI_LOCATION}/conversations`;

// Retries rate limits (429) and server errors (5xx).
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${await auth.getAccessToken()}` } });
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`CX Insights returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Sends every conversation updated after `since` (Unix ms). Returns the new `since`.
export async function syncCxInsights({ since, historical = false }) {
  let newest = since;
  let pageToken;
  do {
    const q = new URLSearchParams({ filter: `update_time>"${new Date(since - 1).toISOString()}"`, view: 'FULL', pageSize: '100' });
    if (pageToken) q.set('pageToken', pageToken);
    const page = await get(`${BASE}?${q}`);
    for (const c of page.conversations ?? []) {
      const id = c.name.split('/').pop();
      const sessionId = `google-cx-insights-${id}`.replace(/:/g, '-');
      const userId = c.obfuscatedUserId ? `google-cx-insights-${c.obfuscatedUserId}`.replace(/:/g, '-') : sessionId;
      const start = toMillis(c.startTime ?? c.createTime);
      const customerChannel = c.callMetadata?.customerChannel;
      let last = 0;
      for (const [i, s] of (c.transcript?.transcriptSegments ?? []).entries()) {
        if (!s.text?.trim()) continue;
        const role = s.segmentParticipant?.role;
        const incoming = role && role !== 'ROLE_UNSPECIFIED' ? role === 'END_USER' : customerChannel != null && s.channelTag === customerChannel;
        const offset = s.words?.[0]?.startOffset; // a duration such as "12.5s", from the start of the conversation
        const at = s.messageTime ? toMillis(s.messageTime) : offset ? start + Math.round(parseFloat(offset) * 1000) : start + i * 1000;
        const ts = Math.max(at, last + 1000); // keep segments at least a second apart
        last = ts;
        await sendToDimensionLabs({
          text: s.text,
          userId,
          incoming,
          sessionId,
          conversationId: sessionId,
          dimensionlabs_timestamp: ts,
          platformJson: { medium: c.medium, language: s.languageCode ?? c.languageCode, agentId: c.agentId, labels: c.labels, role, sentiment: s.sentiment?.score },
        }, { historical: historical || ts < Date.now() - HOUR });
      }
      newest = Math.max(newest, toMillis(c.updateTime));
    }
    pageToken = page.nextPageToken;
  } while (pageToken);
  return newest;
}
```

## 5. Run it

Save the code as `google-cx-insights.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 { syncCxInsights as sync } from './google-cx-insights.mjs';`.

- **History first:** `node run.mjs 2025-01-01` sends every conversation updated since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab. Conversations can carry an expiry time: anything already deleted is gone.
- **Then every 15 minutes:** the scheduled job sends conversations changed since the last run, and they are enriched automatically every day. Turns that are more than an hour old when a conversation reaches CX Insights (often the case for calls ingested after they end) go to `/trackhistorical`, and need a backfill.
- **Re-runs are safe:** duplicates are removed automatically.
- **Rate limits:** CX Insights allows 3,000 requests a minute in total. The code waits whenever Google returns `429`.

You can also [upload a file](/data/upload-a-file) and check it first with the [file checker](/data/check-a-file), then run a backfill.

## Export to BigQuery

CX Insights can write conversations and their analysis to a BigQuery table, and the [BigQuery guide](/integrations/bigquery) sends rows from a table to Dimension Labs.

1. Create the dataset and table first (`bq mk --dataset` and `bq mk --table`). The export writes its own schema, and fails if the table doesn't exist.
2. `POST https://contactcenterinsights.googleapis.com/v1/projects/PROJECT_ID/locations/us-central1/insightsdata:export` with `bigQueryDestination` (`projectId`, `dataset`, `table`) and an optional `filter`, using the same filter terms as above. The export **empties the table first** unless you set `writeDisposition` to `WRITE_APPEND`. It runs as a long-running operation that you can poll.
3. Each row is one conversation, with `conversationName` and a repeated `sentences` field: `sentences.sentence` is the text, `sentences.participantRole` the role (when provided), and `sentences.startOffsetNanos` or `sentences.createTimeNanos` the time. In the BigQuery guide's query, `UNNEST(sentences)` gives one row per sentence, and the mapping above applies.

The export is limited to 60 requests a minute.

## CX Insights reference

- [Filter conversations](https://docs.cloud.google.com/gemini-enterprise-cx/insights/filtering)
- [List conversations](https://docs.cloud.google.com/gemini-enterprise-cx/insights/list-conversations-api)
- [Export conversations to BigQuery](https://docs.cloud.google.com/gemini-enterprise-cx/insights/export)
- [BigQuery export schemas](https://docs.cloud.google.com/gemini-enterprise-cx/insights/bigquery-all-schemas)
- [Quotas and limits](https://docs.cloud.google.com/gemini-enterprise-cx/insights/quotas)
- [Enable Customer Experience Insights](https://docs.cloud.google.com/gemini-enterprise-cx/insights/before-you-begin)
- [Contact Center AI Insights API discovery document](https://contactcenterinsights.googleapis.com/$discovery/rest?version=v1)

Checked against Google's Customer Experience Insights documentation on 2026-10-26.
