Skip to main content
Register an endpoint once, and we’ll POST a signed 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

  1. 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.
  2. On any batch you want notified, pass webhook: { "mode": "svix" }:
That’s it. Webhooks are opt-in per batch — a batch with no 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):
The payload is deliberately thin: ids, status, counts, and your echoed 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.
status has four terminal values, not two. Unlike a single-document webhook, a batch can partially succeed:If you’re porting a handler that branches if status == "Completed", it will silently ignore partially_failed batches and drop the results of every document that did succeed. Treat both completed and partially_failed as “results are ready — inspect counts for per-status breakdown.”

Handling the webhook

Verify the signature, branch on status, 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?

Your svix verification call ports unchanged. Two lines in the handler after verification change:
  • The id field is batch_id, not job_id.
  • Statuses are lowercase (completed / partially_failed / failed / cancelled), and you fetch with GET /v1/batches/{batch_id}. Remember partially_failed — Reducto’s binary Completed/Failed has no equivalent.

Delivery, retries, idempotency

  • Return 2xx within 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-id is 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 the webhook.ping type 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.
The URL must be HTTPS and resolve to a public host. The event body is identical to the signed one above — same four terminal statuses (including 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:
Direct deliveries share everything else with signed ones — the same retry ladder, the same 15-second 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.