# Fireflies Webhooks

This skill targets Webhooks V2, the current scheme. Fireflies steers new
webhook creation to V2 and marks the V1 configuration page as deprecated, so
build new integrations against V2. V1 still works for integrations already on
it and is documented as a [legacy path](#legacy-webhooks-v1) below.

## When to Use This Skill

* Setting up Fireflies.ai webhook handlers (Webhooks V2)
* Debugging Fireflies signature verification failures
* Understanding the `X-Hub-Signature` header and its `sha256=` prefix
* Handling `meeting.transcribed`, `meeting.summarized`, or `meeting.bot_joined`
* Reacting to `meeting_id` / `client_reference_id` from an `uploadAudio` upload
* Migrating an existing V1 handler to V2

## Which Version Am I On?

The two fastest tells are the `sha256=` prefix on the signature header and
the payload field casing — V2 is snake_case, V1 is camelCase:

|  | V2 (this skill's default) | V1 (legacy) |
| --- | --- | --- |
| Header value | `sha256=<hex>` — prefixed | bare hex digest, no prefix |
| Signature header | `X-Hub-Signature` | `x-hub-signature` |
| Event field | `event` | `eventType` |
| Event names | `meeting.transcribed`, `meeting.summarized`, `meeting.bot_joined` | `Transcription completed` |
| Meeting ID field | `meeting_id` | `meetingId` |
| Reference field | `client_reference_id` | `clientReferenceId` |
| Timestamp field | `timestamp` (unix ms) | not sent |
| Signing secret | optional — no header sent when unset | required, 16–32 chars |
| Event selection | subscribe per webhook | none (single event) |
| Configured at | Webhooks V2 configuration page | Settings > Developer Settings |
| Response deadline | 2xx within 10s | not documented |

Header names are case-insensitive, so read them lowercased in both versions. If
the header value starts with `sha256=`, you are on V2.

## Verification (core)

Fireflies V2 signs the raw request body with HMAC-SHA256 keyed on the
signing secret you configured at webhook setup. The digest is hex-encoded,
prefixed with `sha256=`, and sent in the `X-Hub-Signature` header. Compare with
a timing-safe function. The docs state the raw body is what is signed, so there
is no ambiguity here (unlike V1 — see the hedge in the legacy section).

There is no official Fireflies SDK, so verification is manual in every framework.

> The signing secret is optional. If you do not configure one at webhook
> setup, Fireflies sends no `X-Hub-Signature` header at all — confirmed on a
> live test delivery. Decide deliberately: either require a secret and reject
> unsigned deliveries, or accept them with a loud warning. The examples in this
> skill warn and accept so an unconfigured setup works end to end, and reject
> when a secret is configured but the signature is missing or wrong. Configure
> a secret in production.

Node:

```javascript
const crypto = require('crypto');

function verifyFirefliesWebhook(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false; // cannot verify
  if (!signatureHeader.startsWith('sha256=')) return false; // V2 requires the prefix

  const receivedHex = signatureHeader.slice('sha256='.length);
  const expectedHex = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(receivedHex, 'hex'),
      Buffer.from(expectedHex, 'hex')
    );
  } catch {
    return false; // different lengths / non-hex header = invalid
  }
}

```

Python:

```python
import hmac, hashlib

def verify_fireflies_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:  # cannot verify
        return False
    if not signature_header.startswith("sha256="):  # V2 requires the prefix
        return False

    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)

```

> For complete handlers with route wiring, event dispatch, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/fireflies-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/fireflies-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/fireflies-webhooks/examples/fastapi/)

## Common Event Types

Fireflies sends the event name in the JSON body as `event` — there is no
event-type header. You subscribe to events per webhook, and only subscribed
events are delivered.

| `event` value | Triggered When |
| --- | --- |
| `meeting.transcribed` | A meeting has been processed and its transcript is ready |
| `meeting.summarized` | The AI summary for a meeting has been generated |
| `meeting.bot_joined` | The Fireflies notetaker bot joined a meeting |

> For the full webhook reference, see [Fireflies Webhooks V2](https://docs.fireflies.ai/graphql-api/webhooks-v2).

## Payload Structure

```json
{
  "event": "meeting.transcribed",
  "timestamp": 1710876543210,
  "meeting_id": "ASxwZxCstx",
  "client_reference_id": "be582c46-4ac9-4565-9ba6-6ab4264496a8"
}

```

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `event` | string | yes | The event name, e.g. `meeting.transcribed` |
| `timestamp` | number | yes | Unix timestamp in milliseconds |
| `meeting_id` | string | yes | ID of the meeting — the same value as the transcript ID |
| `client_reference_id` | string | no | Custom identifier you set at upload, for correlation |

The webhook is a notification, not the transcript. After verifying, query the
Fireflies GraphQL API with `meeting_id` to fetch sentences, summary, and metadata.

## Important Headers

| Header | Description |
| --- | --- |
| `X-Hub-Signature` | `sha256=` + hex HMAC-SHA256 of the raw body. Omitted entirely when no signing secret is configured. |
| `Content-Type` | `application/json` |
| `User-Agent` | Identifies the sender. A live V2 delivery sent `Fireflies-Webhook/2.0`; the docs' header table still shows `Fireflies-Webhook/1.0`. Do not rely on either value for routing. |
| `X-Webhook-Delivery-Id` | Observed but not documented. A live delivery carried e.g. `test-1784907162698340997`. Useful for logging and idempotency, but treat as best-effort — it is not in the published spec. |

## Environment Variables

```bash
FIREFLIES_WEBHOOK_SECRET=your_signing_secret   # Optional in Fireflies; set one in production

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 fireflies --path /webhooks/fireflies

```

Use the URL Hookdeck prints as your webhook URL on the Fireflies Webhooks V2
configuration page.

## Legacy: Webhooks V1

V1 is deprecated for new integrations — Fireflies redirects new webhook creation
to V2 — but existing V1 webhooks keep delivering. Use this section only when
maintaining one.

V1 signs with HMAC-SHA256 and sends a bare hex digest in `x-hub-signature`
with no `sha256=` prefix. Compare the whole header value directly; do not
strip a prefix, because there isn't one.

```javascript
function verifyFirefliesWebhookV1(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false; // fail closed
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader, 'hex'),
      Buffer.from(expected, 'hex')
    );
  } catch {
    return false;
  }
}

```

> Unconfirmed detail in V1 — which bytes are signed. The header name,
> HMAC-SHA256, hex encoding, and the absence of a `sha256=` prefix are all
> documented for V1. What the V1 docs do not state in prose is whether the
> digest covers the raw request bytes or a re-serialized `JSON.stringify(body)`
> — their code sample links to an external Replit that could not be read. Raw
> body is the default here because it is the safer choice. On your first
> deliveries, log the raw body alongside the header; if verification fails
> consistently with a correct secret, try `JSON.stringify(JSON.parse(rawBody))`
> as the HMAC input before assuming the secret is wrong. This hedge applies to
> V1 only — the V2 docs state plainly that the raw body is signed.

V1 payload and event:

```json
{
  "meetingId": "01HXXXXXXXXXXXXXXXXXXXXXXX",
  "eventType": "Transcription completed",
  "clientReferenceId": "your-optional-upload-reference"
}

```

| `eventType` value | Triggered When |
| --- | --- |
| `Transcription completed` | A meeting has been processed and its transcript is ready |

The V1 secret is a required 16–32 character value set in
app.fireflies.ai/settings > Developer Settings. See
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fireflies-webhooks/references/verification.md#legacy-webhooks-v1-verification)
for the full V1 details and gotchas.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fireflies-webhooks/references/overview.md) - Fireflies webhook concepts, events, and payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fireflies-webhooks/references/setup.md) - Configure the webhook URL, secret, and event subscriptions
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fireflies-webhooks/references/verification.md) - Signature verification details and gotchas