02 Get your data in /Integrations
Fireflies.ai
Send Fireflies.ai meeting transcripts to Dimension Labs with the universal API.
- FA Fireflies.aiTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in each Fireflies.ai meeting transcript as one conversation, 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 Fireflies GraphQL API and posts each turn to the universal API.
What you need
Section titled “What you need”- A Fireflies account whose API key can see the meetings you want. Results are limited to what the key’s owner can see, so use the key of someone who sees your team’s meetings, for example a team admin.
- The email domains your company uses, for example
acme.com, as a fallback for telling your team apart. - A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Fireflies
Section titled “1. Get API access in Fireflies”- Log in at app.fireflies.ai.
- Go to Integrations and click Fireflies API.
- Copy your API key and store it securely.
Send it on every request as Authorization: Bearer YOUR_API_KEY (authorization).
2. Pull new transcripts
Section titled “2. Pull new transcripts”Fireflies has one GraphQL endpoint, https://api.fireflies.ai/graphql. The transcripts query lists transcripts created between two times, with their sentences:
curl -X POST https://api.fireflies.ai/graphql \ -H "Content-Type: application/json" -H "Authorization: Bearer $FIREFLIES_API_KEY" \ -d '{"query":"query($from: DateTime, $to: DateTime) { transcripts(fromDate: $from, toDate: $to, limit: 50, skip: 0) { id date workspace_users meeting_attendees { displayName name email } sentences { index speaker_id speaker_name text } } }","variables":{"from":"2026-10-25T00:00:00.000Z","to":"2026-10-26T00:00:00.000Z"}}'fromDateandtoDatetake ISO 8601 times.limitgoes up to 50; page withskipuntil a page comes back with fewer than 50.- Each sentence has an
index, aspeaker_id, aspeaker_nameand thetext. meeting_attendeeslists each attendee’sdisplayName,nameandemail.workspace_userslists the email addresses of attendees who are Fireflies users on your team.- Errors come back in an
errorsarray in the response body (errors). - The code waits
FIREFLIES_DELAY_HOURS(3 by default) before it reads a transcript, so the meeting has ended and been processed. If recent meetings arrive without turns, raise the delay.
Rate limits, per Fireflies: Free 30 requests a minute and 500 a day; Pro 60 a minute and 5,000 a day; Business and Enterprise 60 a minute per paid seat, pooled across the team, with no daily cap. Daily quotas reset at midnight UTC. Going over returns 429 with a Retry-After header in seconds (limits).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Fireflies | Notes |
|---|---|---|
text | sentences[].text | Consecutive sentences from the same speaker_id are joined into one turn. |
userId | a hash of the speaker’s attendee email, else speaker_id | The code matches speaker_name to an attendee’s displayName or name and sends a hash of their email, never the email or name. Speakers with no match get the transcript id and speaker_id. |
incoming | attendee email against workspace_users | false for your team: the speaker’s email is in workspace_users, or on one of your COMPANY_DOMAINS. Everyone else, and speakers with no matching attendee, are incoming. |
sessionId | transcript id | Prefixed, for example fireflies-01ABC. |
conversationId | transcript id | Same value as sessionId. |
dimensionlabs_timestamp | transcript date | date is in milliseconds. Fireflies doesn’t document the unit of sentence start times, so the code keeps the turns in index order, 1,000 ms apart from date. |
platformJson | duration, calendar_id; speakerMatched, internal | calendar_id is shared by every occurrence of a recurring event. |
Fireflies tells you which attendees are on your team through workspace_users. Teammates who don’t use Fireflies aren’t in it, so the code also checks the email domain against COMPANY_DOMAINS.
Personal data. Attendee names, emails and phone numbers are used only to match speakers and are never sent. Leave out the meeting title, which often names the customer, and meeting_link. Transcripts hold names, companies and numbers: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// fireflies.mjs. Environment: FIREFLIES_API_KEY, COMPANY_DOMAINS (for example "acme.com,acme.co.uk"), FIREFLIES_DELAY_HOURS (default 3), DL_API_KEY.import { createHash } from 'node:crypto';import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const DELAY = Number(process.env.FIREFLIES_DELAY_HOURS ?? 3) * HOUR;const DOMAINS = (process.env.COMPANY_DOMAINS ?? '').toLowerCase().split(',').map((d) => d.trim()).filter(Boolean);const hash = (s) => createHash('sha256').update(s.trim().toLowerCase()).digest('hex').slice(0, 32);const QUERY = `query($from: DateTime, $to: DateTime, $skip: Int) { transcripts(fromDate: $from, toDate: $to, limit: 50, skip: $skip) { id date duration calendar_id workspace_users meeting_attendees { displayName name email } sentences { index speaker_id speaker_name text } }}`;
// Retries Fireflies' rate limits (429) and server errors (5xx), honoring Retry-After.async function graphql(variables) { for (let attempt = 0; ; attempt++) { const res = await fetch('https://api.fireflies.ai/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.FIREFLIES_API_KEY}` }, body: JSON.stringify({ query: QUERY, variables }), }); if (res.ok) { const body = await res.json(); if (body.errors?.length) throw new Error(`Fireflies returned an error: ${JSON.stringify(body.errors)}`); return body.data; } if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Fireflies returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Sends every transcript created at or after `since` (Unix ms) and at least FIREFLIES_DELAY_HOURS ago. Returns the new `since`.export async function syncFireflies({ since, historical = false }) { const until = Date.now() - DELAY; if (until <= since) return since; for (let skip = 0; ; skip += 50) { const { transcripts } = await graphql({ from: new Date(since).toISOString(), to: new Date(until).toISOString(), skip }); for (const t of transcripts ?? []) { const team = new Set((t.workspace_users ?? []).map((e) => e.toLowerCase())); const emails = new Map(); for (const a of t.meeting_attendees ?? []) { for (const n of [a.displayName, a.name]) if (n && a.email) emails.set(n.trim().toLowerCase(), a.email.toLowerCase()); } // Consecutive sentences from one speaker make one turn. const turns = []; for (const s of [...(t.sentences ?? [])].sort((a, b) => a.index - b.index)) { if (!s.text?.trim()) continue; const prev = turns.at(-1); if (prev && prev.speaker_id === s.speaker_id) prev.text += ` ${s.text.trim()}`; else turns.push({ speaker_id: s.speaker_id, speaker_name: s.speaker_name, text: s.text.trim() }); } const session = `fireflies-${t.id}`; const start = toMillis(t.date); turns.forEach((turn, i) => { turn.ts = start + i * 1000; }); for (const turn of turns) { const email = emails.get(turn.speaker_name?.trim().toLowerCase()); const internal = Boolean(email && (team.has(email) || DOMAINS.includes(email.split('@')[1]))); await sendToDimensionLabs({ text: turn.text, userId: email ? `fireflies-${hash(email)}` : turn.speaker_id != null ? `${session}-${turn.speaker_id}` : session, incoming: !internal, sessionId: session, conversationId: session, dimensionlabs_timestamp: turn.ts, platformJson: { durationMinutes: t.duration, calendarId: t.calendar_id, speakerMatched: Boolean(email), internal }, }, { historical: historical || turn.ts < Date.now() - HOUR }); } } if ((transcripts ?? []).length < 50) return until; }}5. Run it
Section titled “5. Run it”Save the code as fireflies.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncFireflies as sync } from './fireflies.mjs';.
- History first:
node run.mjs 2025-01-01sends every transcript since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends transcripts once they are
FIREFLIES_DELAY_HOURSold. By then every turn is more than an hour old, so it goes to/trackhistorical: start a backfill whenever you want the latest meetings enriched. - Re-runs are safe: duplicates are removed automatically. If a transcript was late, run
node run.mjswith a date before the meeting to pick it up.
Each page of up to 50 transcripts is one request. On the Free and Pro plans, the daily quota caps how much history one day’s run can load: the code waits whenever Fireflies asks it to, so a history load that hits the daily cap pauses until the quota resets at midnight UTC. Run long history loads from your computer rather than on Lambda. You can also upload a file of exported transcripts (see Call transcripts, and check it with the file checker), then run a backfill.
Fireflies reference
Section titled “Fireflies reference”- Authorization
- Transcripts query
- Transcript query
- Transcript schema
- Sentence schema
- MeetingAttendeeModel schema
- Limits
- Errors
Checked against Fireflies’ documentation on 2026-10-26.