# Fathom Webhooks

Fathom is an AI meeting notetaker. After a recorded meeting is processed, Fathom
POSTs the meeting (optionally with its summary, transcript, action items and CRM
matches) to your webhook URL. This skill covers **Fathom the notetaker**
(fathom.video / developers.fathom.ai), not Fathom Analytics (usefathom.com).

## When to Use This Skill

* How do I receive Fathom webhooks after a meeting is recorded?
* How do I verify Fathom webhook signatures (`webhook-signature`)?
* How do I get the Fathom meeting summary, transcript, or action items via webhook?
* Why is my Fathom webhook signature verification failing?
* Why isn't my Fathom webhook firing?

## Verification (core)

Fathom uses Standard Webhooks. Every request carries
`webhook-id`, `webhook-timestamp` (Unix seconds) and `webhook-signature`
(space-delimited `v1,<base64>` list). The signature is base64(HMAC-SHA256) over
`{webhook-id}.{webhook-timestamp}.{raw body}`, keyed with the **base64-decoded**
part of your `whsec_...` secret.

The official SDKs wrap this in one call. It throws on a bad signature or a
timestamp more than 5 minutes off, and returns the parsed payload:

```javascript
import { Fathom } from 'fathom-typescript';   // Python: from fathom_python import Fathom
const meeting = Fathom.verifyWebhook(process.env.FATHOM_WEBHOOK_SECRET, headers, rawBody);

```

Manual equivalent (Node), if you can't use the SDK:

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

function verifyFathomWebhook(secret, id, timestamp, signatureHeader, rawBody) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.split('_')[1], 'base64'); // decode part after whsec_
  const expected = crypto.createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`).digest('base64');
  return signatureHeader.split(' ').some((entry) => {
    const sig = entry.split(',')[1] ?? entry;               // strip "v1,"
    try { return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }
    catch { return false; }                                 // length mismatch
  });
}

```

Use the **raw body** (before JSON parsing). Re-serialized JSON will not match.

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

## Webhook Payload

Fathom sends **one** webhook type, *new meeting content ready*, fired once per
meeting when the recording finalizes (the same moment the summary email goes out).
The body is the Meeting object. **There is no event-type field** to dispatch on.

| Field | Notes |
| --- | --- |
| `title`, `meeting_title` | Recording title; calendar event title (nullable) |
| `recording_id` | Integer ID of the recording |
| `url`, `share_url` | Links to the recording in Fathom |
| `created_at`, `recording_start_time`, `recording_end_time` | ISO 8601 |
| `recorded_by` | `{ name, email, email_domain, team }` |
| `calendar_invitees` | `[{ name, email, email_domain, is_external }]` |
| `shared_with` | `no_teams`, `single_team`, `multiple_teams`, `all_teams` |
| `default_summary` | `{ template_name, markdown_formatted }` if `include_summary` |
| `transcript` | `[{ speaker: { display_name }, text, timestamp }]` if `include_transcript` |
| `action_items` | `[{ description, completed, assignee, ... }]` if `include_action_items` |
| `crm_matches` | `{ contacts, companies, deals }` if `include_crm_matches` |

## Important Headers

| Header | Description |
| --- | --- |
| `webhook-id` | Unique message ID; **unchanged on retries**, so use it for idempotency |
| `webhook-timestamp` | Unix timestamp in seconds |
| `webhook-signature` | `v1,<base64>` entries, space-delimited |

## Environment Variables

```bash
FATHOM_WEBHOOK_SECRET=whsec_xxxxx   # One secret per webhook, shown when it is created

```

## Local Development

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

```

Set the printed URL as the webhook's Destination URL in Fathom (Settings → API
Access → Manage → Add Webhook), then use **Send test payload** or record a short meeting.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fathom-webhooks/references/overview.md) - What Fathom webhooks are, payload fields, delivery behaviour
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fathom-webhooks/references/setup.md) - Create a webhook in Settings or via the API, get the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/fathom-webhooks/references/verification.md) - Signature verification details, manual Python, gotchas