batch.update event to it the moment a batch reaches a terminal state. This is the production alternative to polling GET /v1/batches/{id} in a loop.
The signature scheme is wire-compatible with Svix, so if you already verify Svix or Reducto webhooks, your verification code works here unchanged — you verify with the standard open-source svix library.
Prefer webhooks for production workflows. Polling remains available, but
GET /v1/batches/{id} is limited to 200 requests per second per organization and may return 429 Too Many Requests when that limit is exceeded.Setup
- In the dashboard, add an endpoint (an HTTPS URL). We show you a signing secret (
whsec_…) once at creation — store it as a secret in your app. You can re-reveal or rotate it anytime from the dashboard. - On any batch you want notified, pass
webhook: { "mode": "svix" }:
webhook field never fires one, even if you have endpoints registered. When you do opt in, every enabled endpoint on your org receives the event.
The event
When the batch finishes we POST this body (and only this — no results inline):metadata. To get the actual extraction, call GET /v1/batches/{batch_id} and fetch each item’s result. results_expires_at is the deadline after which those results are deleted (3-day default retention) — fetch before then. Need longer? Email hello@hanji.dev.
Handling the webhook
Verify the signature, branch onstatus, return 2xx fast, and do the real work after. Headers: svix-id, svix-timestamp, svix-signature (the Standard-Webhooks aliases webhook-id / webhook-timestamp / webhook-signature are also sent).
Coming from Reducto?
Yoursvix verification call ports unchanged. Two lines in the handler after verification change:
- The id field is
batch_id, notjob_id. - Statuses are lowercase (
completed/partially_failed/failed/cancelled), and you fetch withGET /v1/batches/{batch_id}. Rememberpartially_failed— Reducto’s binaryCompleted/Failedhas no equivalent.
Delivery, retries, idempotency
- Return
2xxwithin 15 seconds. Do slow work asynchronously; we only read the status code. - Retries. A non-
2xx(or a timeout) is retried on an escalating schedule — 8 attempts over ~27 hours — so a receiver that’s briefly down still gets the event. After the ladder is exhausted the delivery is marked failed and is resendable from the dashboard. - Be idempotent.
svix-idis stable across every retry of one event — use it as your dedup key. At-least-once delivery means you may occasionally see the same event twice. - Redirects are not followed. Point the endpoint at the final URL.
Testing
- Send a test ping from the dashboard — it delivers a
{"type": "webhook.ping"}event so you can confirm your endpoint and signature verification work before wiring up a real batch. Short-circuit on thewebhook.pingtype as shown above. - Inspect payloads with a throwaway endpoint from webhook.site while you build.
- Watch deliveries in the dashboard: every attempt, its status code, and a per-row Resend (available while the batch’s results are still within their 3-day window).
Direct mode (prototyping)
For quick tests or a dynamic per-request destination, pass the URL inline instead of registering an endpoint. Direct deliveries are unsigned — best for prototyping or internal integrations. Use signed registered endpoints (above) for production.partially_failed).
Since there’s no signature, authenticate by round-tripping a secret token through metadata (we echo it back verbatim) and checking it in your handler:
2xx deadline, and the same SSRF protections (private, loopback, and cloud-metadata destinations are refused). Don’t put PHI in metadata unless the receiving endpoint is inside your compliance boundary; it’s echoed verbatim to whatever URL the request names.