Skip to main content
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>
{
  "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.
Respond with any 2xx within 10 seconds. Ack fast and process async — a slow response counts as a failed attempt.

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):
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 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.