02 Get your data in /Integrations
Google Meet
Send Google Meet transcripts to Dimension Labs with the universal API.
- GM Google MeetTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in Google Meet transcripts, one conversation per transcript, turn by turn: what your prospects and customers said, and what your own team said. You run it on your side: a small script pulls from the Google Meet REST API and posts each turn to the universal API.
What you need
Section titled “What you need”- Transcription turned on in the meetings you want, on a Google Workspace edition that supports it. Only meetings that were transcribed have a transcript.
- A Google Cloud project with the Google Meet REST API and the Admin SDK API turned on, and a service account with a JSON key.
- A Google Workspace super administrator to set up domain-wide delegation for that service account.
- The users whose meetings you want, for example your sales and success teams, and one admin user for directory lookups.
- Optionally, the email domains your company uses, for example
acme.com, to narrow who counts as your team. - A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Google Meet
Section titled “1. Get API access in Google Meet”- In the Google Cloud console, go to IAM & Admin > Service Accounts, open your service account, and under Domain-wide delegation copy its Client ID. Create and download a JSON key for it.
- In the Google Admin console, as a super administrator, go to Security > Access and data control > API controls and click Manage Domain Wide Delegation. Click Add new, paste the client ID, and enter these scopes:
https://www.googleapis.com/auth/meetings.space.readonly,https://www.googleapis.com/auth/admin.directory.user.readonly. Click Authorize. Google says changes can take up to 24 hours (create credentials). - The service account now acts as each user you name. Meet returns the conferences that user owns or took part in (work with artifacts).
meetings.space.readonly lets the app read metadata about any meeting space the user has access to (scopes).
2. Pull new transcripts
Section titled “2. Pull new transcripts”List conferences that ended since your cursor, then each conference’s transcripts and their entries:
curl -G -H "Authorization: Bearer $GOOGLE_TOKEN" "https://meet.googleapis.com/v2/conferenceRecords" \ --data-urlencode 'filter=end_time>="2026-10-25T00:00:00Z" AND end_time<="2026-10-26T00:00:00Z"'
curl -H "Authorization: Bearer $GOOGLE_TOKEN" "https://meet.googleapis.com/v2/conferenceRecords/RECORD_ID/transcripts"curl -H "Authorization: Bearer $GOOGLE_TOKEN" "https://meet.googleapis.com/v2/conferenceRecords/RECORD_ID/transcripts/TRANSCRIPT_ID/entries?pageSize=100"- conferenceRecords.list filters on
start_timeorend_time. Every list returns anextPageTokenwhile there are more pages. - A transcript’s
stateisSTARTED,ENDED(the file isn’t generated yet) orFILE_GENERATED. The code skips transcripts that are stillSTARTED. - transcripts.entries.list returns one entry per stretch of one person’s speech:
participant,text,languageCode,startTimeandendTime. Entries can differ from the Google Doc if speakers interleave within milliseconds or someone edited the Doc. - participants.list says who each
participantis: asignedinUser(with auserid anddisplayName), ananonymousUseror aphoneUser. - The code waits
MEET_DELAY_HOURS(1 by default) after a conference ends before it reads it, so the transcript has ended.
Quotas, per Google: 6,000 read requests per minute per project, and 600 per minute per user per project. Going over returns 429; back off and retry (usage limits).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Google Meet | Notes |
|---|---|---|
text | entry text | One message per entry. |
userId | signedinUser.user id, else a hash of the participant | The user id is stable across meetings and works with the Admin SDK. Anonymous and phone participants get a hash of the participant resource name, never their name or number. |
incoming | signedinUser.user looked up in your directory | false when the Admin SDK finds the user in your Google Workspace (and, if you set COMPANY_DOMAINS, their primary email is on one of those domains). Signed-in users from other organizations, anonymous and phone participants are incoming. |
sessionId | conference record and transcript ids | For example meet-abc123-def456. A conference can have more than one transcript session. |
conversationId | conference record id | Keeps several transcripts of one conference together. |
dimensionlabs_timestamp | entry startTime | Absolute UTC time. Entries closer than a second apart are spaced to 1,000 ms. |
platformJson | languageCode, space, internal | space is the meeting space, shared by every conference held in it. |
Google Meet doesn’t mark participants as internal or external, so the code asks your own directory: users.get accepts the user’s unique id, and only finds users in your Google Workspace account.
Personal data. Display names, emails and partially redacted phone numbers are never sent. Transcripts hold names, companies and numbers: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”Install the auth library in your project first: npm install google-auth-library.
// google-meet.mjs. Environment: MEET_SERVICE_ACCOUNT_KEY (path to the JSON key), MEET_USERS (emails whose meetings to read,// comma-separated), MEET_ADMIN (an admin email, for directory lookups), COMPANY_DOMAINS (optional), MEET_DELAY_HOURS (default 1), DL_API_KEY.import { readFileSync } from 'node:fs';import { createHash } from 'node:crypto';import { JWT } from 'google-auth-library';import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const MEET = 'https://meet.googleapis.com/v2';const MEET_SCOPE = 'https://www.googleapis.com/auth/meetings.space.readonly';const DIR_SCOPE = 'https://www.googleapis.com/auth/admin.directory.user.readonly';const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const DELAY = Number(process.env.MEET_DELAY_HOURS ?? 1) * HOUR;const csv = (s) => (s ?? '').toLowerCase().split(',').map((x) => x.trim()).filter(Boolean);const DOMAINS = csv(process.env.COMPANY_DOMAINS);const key = JSON.parse(readFileSync(process.env.MEET_SERVICE_ACCOUNT_KEY, 'utf8'));const hash = (s) => createHash('sha256').update(s).digest('hex').slice(0, 32);const clean = (s) => s.replace(/[^A-Za-z0-9_-]/g, '_');const clients = new Map();
// Domain-wide delegation: the service account acts as `subject`.function client(subject, scope) { const id = `${subject} ${scope}`; if (!clients.has(id)) clients.set(id, new JWT({ email: key.client_email, key: key.private_key, scopes: [scope], subject })); return clients.get(id);}
// Retries quota errors (429) and server errors (5xx) with backoff. With optional, other 4xx return null.async function get(url, subject, scope, optional = false) { for (let attempt = 0; ; attempt++) { const { token } = await client(subject, scope).getAccessToken(); const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) return res.json(); if (optional && res.status !== 429 && res.status < 500) return null; if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Google returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
async function list(path, field, subject, params = {}) { const items = []; for (let pageToken = ''; ; ) { const qs = new URLSearchParams({ ...params, ...(pageToken ? { pageToken } : {}) }); const page = await get(`${MEET}/${path}?${qs}`, subject, MEET_SCOPE); items.push(...(page[field] ?? [])); pageToken = page.nextPageToken; if (!pageToken) return items; }}
// Your team is outgoing: the user is in your Google Workspace directory (and on COMPANY_DOMAINS, if set).const directory = new Map();async function isInternal(participant) { const user = participant?.signedinUser?.user; if (!user) return false; if (!directory.has(user)) { const u = await get(`https://admin.googleapis.com/admin/directory/v1/users/${user.split('/')[1]}`, process.env.MEET_ADMIN, DIR_SCOPE, true); directory.set(user, Boolean(u && (!DOMAINS.length || DOMAINS.includes(u.primaryEmail?.split('@')[1]?.toLowerCase())))); } return directory.get(user);}
// Sends every conference that ended at or after `since` (Unix ms) and at least MEET_DELAY_HOURS ago. Returns the new `since`.export async function syncMeet({ since, historical = false }) { const until = Date.now() - DELAY; if (until <= since) return since; const filter = `end_time>="${new Date(since).toISOString()}" AND end_time<="${new Date(until).toISOString()}"`; const records = new Map(); // a conference several users attended is read once for (const subject of csv(process.env.MEET_USERS)) { for (const r of await list('conferenceRecords', 'conferenceRecords', subject, { filter, pageSize: '100' })) { if (!records.has(r.name)) records.set(r.name, { record: r, subject }); } } for (const { record, subject } of records.values()) { const people = new Map((await list(`${record.name}/participants`, 'participants', subject, { pageSize: '250' })).map((p) => [p.name, p])); const conversation = `meet-${clean(record.name.split('/')[1])}`; for (const t of await list(`${record.name}/transcripts`, 'transcripts', subject, { pageSize: '100' })) { if (t.state === 'STARTED') continue; const session = `${conversation}-${clean(t.name.split('/').pop())}`; let last = 0; for (const e of await list(`${t.name}/entries`, 'transcriptEntries', subject, { pageSize: '100' })) { if (!e.text?.trim()) continue; const p = people.get(e.participant); const internal = await isInternal(p); const ts = Math.max(toMillis(e.startTime), last + 1000); // keep entries at least a second apart last = ts; await sendToDimensionLabs({ text: e.text, userId: p?.signedinUser?.user ? `meet-${clean(p.signedinUser.user.split('/')[1])}` : e.participant ? `meet-${hash(e.participant)}` : session, incoming: !internal, sessionId: session, conversationId: conversation, dimensionlabs_timestamp: ts, platformJson: { language: e.languageCode, space: record.space, internal }, }, { historical: historical || ts < Date.now() - HOUR }); } } } return until;}5. Run it
Section titled “5. Run it”Save the code as google-meet.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncMeet as sync } from './google-meet.mjs';.
- History first:
node run.mjs 2026-09-27, a date within the last 30 days, sends every transcript the API still holds to/trackhistorical. Then start a backfill from the Backfill tab. Entries for older conferences are already deleted from the API. - Then every 15 minutes: the scheduled job sends conferences once they ended
MEET_DELAY_HOURSago. Most turns are more than an hour old by then, so they go to/trackhistorical: start a backfill whenever you want the latest meetings enriched. - Re-runs are safe: duplicates are removed automatically, including the same conference seen through two of your users.
- If every turn arrives as incoming, the directory lookups are failing: check that
MEET_ADMINis an admin and that the directory scope is in the domain-wide delegation entry. - Never stop for more than a month: if the job is down longer than 30 days, the API has deleted the missed entries.
Each conference costs a few read requests, well within Google’s per-minute quotas. For meetings older than 30 days, export the transcript Google Docs from Drive, turn them into a file (see Call transcripts), check it with the file checker and upload it, then run a backfill.
Google Meet reference
Section titled “Google Meet reference”- Work with artifacts (transcripts, retention)
- conferenceRecords.list
- Transcript resource
- transcripts.entries.list
- Participant resource
- Meet REST API scopes and domain-wide delegation
- Create credentials (domain-wide delegation)
- Admin SDK: users.get
- Usage limits
Checked against Google’s documentation on 2026-10-26.