export const metadata = {
  title: "Webhooks",
  description:
    "Receive signed HTTP notifications when FaithTranscripts transcripts and AI validation passes complete.",
  alternates: { canonical: "/docs/api/webhooks" },
};

# Webhooks

Register HTTPS endpoints to receive event notifications when transcripts
and AI passes reach terminal states. Payloads are signed with HMAC-SHA256
using the endpoint's signing secret so you can verify authenticity.

<Callout variant="tip">
  **Not a developer?** Our [Zapier](/integrations/zapier) and
  [n8n](/integrations/n8n) guides walk through the same webhook flow with
  no-code steps and a drop-in HMAC verification snippet.
</Callout>

## Register an endpoint

Add, edit, pause, and remove endpoints from
[Settings → Webhooks](/settings/webhooks) in the dashboard. The API does not
expose routes to manage webhook configuration — only the dashboard can define
where we deliver events.

When you create an endpoint, we show a `signing_secret` (format: `whsec_…`)
which you'll need for verification. You can reveal it again from Settings at any
time.

## Event types

| Event                  | When it fires                                  |
| ---------------------- | ---------------------------------------------- |
| `transcript.created`   | A new transcript has been created.             |
| `transcript.completed` | Deterministic pass finished successfully.      |
| `transcript.failed`    | Processing failed at some stage.               |
| `ai_pass.started`      | AI validation pass started.                    |
| `ai_pass.completed`    | AI validation pass finished successfully.      |
| `ai_pass.failed`       | AI validation pass failed.                     |

## Delivery format

Each delivery is a `POST` with `Content-Type: application/json`:

```http
POST /hooks/ft HTTP/1.1
Host: client.example.com
Content-Type: application/json
X-FT-Event: transcript.completed
X-FT-Delivery: evt_01HW...
X-FT-Signature: 3ac15e2dfa...
```

The body is the current resource state (same shape as `GET /transcripts/{id}`)
with two added fields:

```json
{
  "id": "tr_01HW...",
  "object": "transcript",
  "status": "completed",
  "content": "...",
  "event": "transcript.completed",
  "delivery_id": "evt_01HW..."
}
```

## Verifying the signature

Verify by computing `HMAC-SHA256(secret, raw_body)` and comparing against
the `X-FT-Signature` header in constant time.

### Node / Bun

```ts
import crypto from "node:crypto";

export function verifyFtSignature(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signature, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

### Python

```python
import hmac, hashlib

def verify_ft_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

<Callout variant="warn">
  Sign over the **raw request body bytes**, not the parsed JSON. Body
  parsing may reorder keys or re-encode characters, breaking the signature
  check.
</Callout>

## Retries and ordering

- **Retries**: on any non-2xx response we retry up to 5 times with exponential backoff: 10s, 1m, 10m, 1h, 6h.
- **Ordering**: deliveries for a single resource are serialized, but newer events may arrive before older retries succeed. Treat each event as "the current state of this resource as of its timestamp" rather than a strict ordered stream.
- **Deduplication**: use the `delivery_id` (also in the `X-FT-Delivery` header) as an idempotency key.
- **Replay**: the dashboard shows delivery history and exposes a manual **Replay** button for any delivery.

## Rotating the signing secret

Rotate at any time from [Settings → Webhooks](/settings/webhooks). Existing
deliveries are not resigned; the new secret applies to every delivery sent after
rotation.

## Pausing and deleting

Use [Settings → Webhooks](/settings/webhooks) to pause an endpoint (disable
deliveries without deleting it) or remove it entirely.

## Testing

You can hit any request-bin service (e.g. webhook.site) for development.
Payloads are real — they reflect actual transcript state.
