API and webhooks

Keys, the /v1 routes, and verifying a delivery signature.

Everything the review UI does over an episode is available over HTTP at /v1. Issue a key from API keys — it is shown once, because only its hash is stored.

Authentication

curl https://your-host/v1/me \
  -H "Authorization: Bearer cf_live_…"

A key carries a role, the same ladder the web routes use. /v1 accepts a Bearer key only and never a cookie, which is what makes it safe to exempt from CSRF — a browser session gets a 401 there.

Routes

MethodPathReturns
GET/v1/mePlan, seats, credits remaining.
GET/v1/episodesYour episodes.
GET/v1/episodes/{id}One episode with its status.
GET/v1/episodes/{id}/clipsCandidates and renders.
GET/v1/clips/{render_id}One clip and its asset URLs.
GET/POST/v1/webhooksList or register an endpoint.
DELETE/v1/webhooks/{id}Remove an endpoint.
GET/v1/webhooks/deliveriesRecent attempts and their outcomes.

Webhooks

Register a URL and we post to it when a clip becomes ready. Delivery rides the same durable job queue as everything else, so retries, backoff and dead-lettering already apply — a receiver that is down for ten minutes does not lose the event.

Each request carries a signature header shaped t=<unix>,v1=<hmac> — deliberately the same construction Stripe uses, so if you already verify Stripe webhooks you can reuse that verifier. Sign "{timestamp}.{body}" with your endpoint secret using HMAC-SHA256 and compare in constant time. Reject anything older than a few minutes.

Private addresses are refused.

A registered URL is resolved and rejected if it points at loopback, link-local or private space — the same guard that protects URL ingest. On a cloud host, an unguarded fetch of 169.254.169.254 is a credential leak.