# Fireflies.ai

> Send Fireflies.ai meeting transcripts to Dimension Labs with the universal API.

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](/api/tracker).

## 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](/api/tracker#your-api-key).

## 1. Get API access in Fireflies

1. Log in at [app.fireflies.ai](https://app.fireflies.ai).
2. Go to **Integrations** and click **Fireflies API**.
3. Copy your API key and store it securely.

Send it on every request as `Authorization: Bearer YOUR_API_KEY` ([authorization](https://docs.fireflies.ai/fundamentals/authorization)).

## 2. Pull new transcripts

Fireflies has one GraphQL endpoint, `https://api.fireflies.ai/graphql`. The [`transcripts` query](https://docs.fireflies.ai/graphql-api/query/transcripts) lists transcripts created between two times, with their sentences:

```bash
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"}}'
```

- `fromDate` and `toDate` take ISO 8601 times. `limit` goes up to 50; page with `skip` until a page comes back with fewer than 50.
- Each [sentence](https://docs.fireflies.ai/schema/sentence) has an `index`, a `speaker_id`, a `speaker_name` and the `text`.
- [`meeting_attendees`](https://docs.fireflies.ai/schema/meeting-attendee) lists each attendee's `displayName`, `name` and `email`. `workspace_users` lists the email addresses of attendees who are Fireflies users on your team.
- Errors come back in an `errors` array in the response body ([errors](https://docs.fireflies.ai/fundamentals/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](https://docs.fireflies.ai/fundamentals/limits)).

## 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](/data/pii-redaction).

## 4. Send it

```js
// fireflies.mjs. Environment: FIREFLIES_API_KEY, COMPANY_DOMAINS (for example "acme.com,acme.co.uk"), FIREFLIES_DELAY_HOURS (default 3), DL_API_KEY.



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

Save the code as `fireflies.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 { syncFireflies as sync } from './fireflies.mjs';`.

- **History first:** `node run.mjs 2025-01-01` sends every transcript since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends transcripts once they are `FIREFLIES_DELAY_HOURS` old. 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.mjs` with 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](/data/upload-a-file) of exported transcripts (see [Call transcripts](/transform/recipe-call-transcripts), and check it with the [file checker](/data/check-a-file)), then run a backfill.

## Fireflies reference

- [Authorization](https://docs.fireflies.ai/fundamentals/authorization)
- [Transcripts query](https://docs.fireflies.ai/graphql-api/query/transcripts)
- [Transcript query](https://docs.fireflies.ai/graphql-api/query/transcript)
- [Transcript schema](https://docs.fireflies.ai/schema/transcript)
- [Sentence schema](https://docs.fireflies.ai/schema/sentence)
- [MeetingAttendeeModel schema](https://docs.fireflies.ai/schema/meeting-attendee)
- [Limits](https://docs.fireflies.ai/fundamentals/limits)
- [Errors](https://docs.fireflies.ai/fundamentals/errors)

Checked against Fireflies' documentation on 2026-10-26.
