Skip to main content
Instead of polling GET /v3/jobs/{job_id} until a job finishes, register a webhook and Hedra POSTs the result to you. A job fires exactly one terminal event — job.completed or job.failed — chosen from its final status when the delivery goes out.

Register a webhook

There are two ways to say where deliveries go:
  • Per job — pass webhook at submit, next to input. This URL applies to that job only.
  • Account default — store one endpoint with PUT /webhooks/default. It receives the terminal event for every job that names no per-job webhook.
A per-job URL always wins over the default; the default only covers jobs that didn’t set one.
Setting enabled: false pauses the default endpoint without discarding the URL; DELETE /webhooks/default removes it entirely. To check the wiring before running a real job, POST /webhooks/default/test fires a test delivery at the stored endpoint and reports what it answered:

The payload

The body is the same envelope GET /v3/jobs/{job_id} returns, minus its poll-only fields (logs, cost, currency) — poll the job if you need those.
On job.failed, status is FAILED and error carries the same error envelope the poll endpoint returns.
Generated media is retained for 48 hours after a job completes, measured from the original completion time. Download outputs[].url (or chain outputs[].asset_id) promptly — a delivery replayed after the window carries EXPIRED outputs with url: null.

Verify the signature

Every delivery is signed with Hedra’s ed25519 key — the same key for every account, so you can fetch it once and cache it:
Each POST carries these headers: The signature covers a canonical string of five newline-separated fields, in this order:
Hash the body exactly as received, before any JSON parsing or re-serialization:
The signature deliberately covers the deduplication id and the redelivery flag, not just the body — both decide whether you process a duplicate, so an unsigned copy of either would let anyone who captured a delivery replay it past your idempotency check. Verify before acting on any header.

Retries and deduplication

Delivery is at-least-once. A 2xx from your endpoint is success; anything else — including a redirect, which is never followed — is retried. Hedra makes up to 12 attempts over approximately 6 hours, backing off 10s, 30s, 90s, 4m30s, 13m30s, 40m30s, then hourly. Because the retry window is bounded, acknowledge with a 2xx first and do your own processing asynchronously. Deduplicate on X-Hedra-Webhook-Id. It identifies the event — it is the job’s own id and is byte-identical across every retry and replay. Do not hash the request body: each attempt re-signs the output URLs, so the body legitimately differs between attempts of the same event. The one exception is X-Hedra-Webhook-Redelivery: true: an operator asked for this event to be sent again, so process it even if you have already recorded that id — that request is the whole reason it was sent.

Inspect and replay deliveries

GET /v3/webhooks/deliveries lists every delivery with its status (PENDING, DELIVERING, DELIVERED, FAILED), its source (per_job or default), cumulative attempts, the latest outcome (last_response_status, last_error), and its replay history.
An endpoint that is unreachable for the whole retry window is marked FAILED and not retried again. Replay it:
A replay re-sends on the same delivery record — the webhook id stays the same, every attempt of the replayed cycle carries X-Hedra-Webhook-Redelivery: true, and the previous outcome is archived in the delivery’s redeliveries list. A replay answers 409 while a delivery for the job is still in flight. last_error is a structured error envelope, not free text: a stable code (DEADLINE_EXCEEDED, UNAVAILABLE, RESOURCE_EXHAUSTED, and so on — the same vocabulary the rest of the API uses), a fixed message, and retryable, which tells you whether replaying is likely to help. One code is permanent and stops the ladder immediately: FAILED_PRECONDITION means the URL resolves to a blocked address range (or redirects to one) — fix the URL; replaying will not help. Your endpoint’s URL, headers, and response body are never echoed back, so treat your own logs as the record of what your endpoint returned.