> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parable.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Signed capture.processed pushes to your HTTPS endpoint

Instead of (or alongside) polling, Parable can POST each capture to an
HTTPS endpoint you provide, within about a minute of it entering the
stream. Parable provisions the endpoint and hands you its **signing
secret** once, over a secure channel.

By default the webhook starts "from now": only captures processed after the
endpoint is created get pushed, and earlier history stays reachable via the
pull API. A full-history push at creation is available on request.

## The request

```
POST <your endpoint URL>
content-type: application/json
x-parable-event: capture.processed
x-parable-delivery: <delivery id, uuid>
x-parable-signature: t=<unix seconds>,v1=<hex hmac>
```

```json theme={null}
{
  "event": "capture.processed",
  "delivery_id": "…",
  "emitted_at": "2026-07-02T18:09:41.000Z",
  "data": { "...": "document, same schema as the pull API" }
}
```

`data` is a full [transcript document](/transcript-document).

<Note>
  Respond with any 2xx within **10 seconds**. Ack fast and process async — a
  slow response counts as a failed attempt.
</Note>

## Verifying the signature

Reject any delivery whose signature you can't verify — without this check,
anyone who discovers your endpoint URL can feed you fabricated transcripts.

`v1` is `hmac_sha256(secret, "<t>.<raw body>")` in hex, computed over the
raw request body bytes (don't re-serialize the JSON):

```js theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const { t, v1 } = Object.fromEntries(
    signatureHeader.split(",").map((kv) => kv.split("=", 2)),
  );
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // stale
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return (
    expected.length === v1.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
  );
}
```

## Delivery semantics

* **At-least-once.** Rarely, a delivery may arrive twice (for example your
  2xx was sent but not observed). Dedupe on `data.id` — the capture id is
  the stable key; `delivery_id` identifies the delivery attempt chain.
* **Retries.** A failed attempt (non-2xx, timeout, connection error)
  retries after 1m, then 5m, 15m, 1h, 6h, 24h before giving up — about 31
  hours of tolerance for an outage on your side.
* **Recovery.** Anything that exhausts retries is still in the pull
  stream — resume your [cursor loop](/quickstart#3-run-the-cursor-loop) to
  catch up; nothing is ever lost.
* **Snapshot payloads.** The document is rendered at send time. Later fixes
  (for example a speaker rename) don't re-push; re-fetch
  `GET /api/v1/captures/:id` for current state.
* **Ordering is not guaranteed** across in-flight deliveries; use
  `started_at` / `ended_at` for display ordering, as with the pull API.
