Skip to main content

Troubleshooting: webhook lifecycle error codes

When a webhook flow breaks, the error code tells you where in the endpoint lifecycle it broke — before the endpoint exists, while a delivery is being attempted, or as the receiver processes (or rejects) the event. This page maps each webhook code to its lifecycle state and gives you the detection query and the fix for each one. For the deep-dive runbook behind any single code, follow the cross-links at the bottom.

1. The webhook endpoint lifecycle

An endpoint moves through four states, and each code fires in exactly one of them:
  1. createdPOST /api/v1/webhooks accepts the URL, validates it, and persists the endpoint.
  2. delivery-attempt — Orbit POSTs the event to your URL and waits for a 2xx ack.
  3. delivery-failed — the attempt got a non-2xx response, a timeout, or a receiver-side exception; the retry cadence starts.
  4. retry / DLQ — up to 9 retries over ~4.3 hours, then the row moves to the dead-letter queue until you requeue or replay it.
Match the code to the state first — it decides which fix applies:

2. The codes

WEBHOOK_DELIVERY_FAILED — receiver answered non-2xx (HTTP 502)

Orbit POSTed the event and got back a 404, 5xx, or a timeout instead of a 2xx. Anything outside the 2xx range counts as a failed attempt, so the retry cadence starts and the row heads for the DLQ if it keeps failing.

WEBHOOK_PROCESSING_FAILED — receiver threw (HTTP 500)

Your handler accepted the request but raised an exception — an unhandled throw, a crashed downstream call — while processing it. From Orbit’s side this is a 5xx ack, identical in shape to WEBHOOK_DELIVERY_FAILED; the registry records it separately because the fix is in your handler’s code, not in routing or uptime.

WEBHOOK_SIGNATURE_INVALID — HMAC mismatch (HTTP 401)

Your verifier rejected the delivery: the computed HMAC matched none of the v1 candidates in the signature header. This is the same failure class as INVALID_SIGNATURE, reported from the delivery-log side. Until verification passes, every delivery fails in the delivery-attempt state.

Creation-time siblings

The three created-state rejections are expanded in Troubleshooting: webhook endpoint creation errors: WEBHOOK_ENDPOINT_CAP_REACHED (tenant already at 10 endpoints), WEBHOOK_DNS_INVALID (hostname does not resolve), and INVALID_WEBHOOK_URL (non-HTTPS scheme, or a private/loopback/internal address blocked by safety validation). All three answer 422 before any endpoint is persisted.

The dedup sibling

Retry and DLQ states re-deliver the same event id — that is at-least-once semantics, not a new error code. If your consumer treats the re-delivery as a new event, the failure is a consumer-side dedup gap; see Troubleshooting: duplicate webhook events and consumer-side dedup for the event-id pattern that makes retries and replays safe.

3. Detecting each code

List the deliveries for the endpoint and filter to the failures:
Then pull one row (GET /api/v1/webhooks/{id}/deliveries/{deliveryId}) and read the HTTP status your endpoint returned:
  • 401 → signature path. Your verifier rejected the HMAC; work the WEBHOOK_SIGNATURE_INVALID fix below.
  • 404 / 5xx / timeout → delivery or processing path. A 404 means the route moved; a 500 from your handler means it threw; a timeout means it ran past your endpoint’s timeout_seconds.
For the signature path specifically, wrap your verifier so a rejection logs the header shape before you return 401:
The reject path must return false, not throw — a throwing verifier turns every HMAC mismatch into a WEBHOOK_PROCESSING_FAILED (500) instead of the WEBHOOK_SIGNATURE_INVALID (401) you can actually diagnose.

4. Fix per code

Non-2xx path — WEBHOOK_DELIVERY_FAILED / WEBHOOK_PROCESSING_FAILED

  • Ack within the delivery window. Your endpoint must return a 2xx within the window documented on Webhook security (30 seconds by default, or your endpoint’s timeout_seconds override). Move heavy work to a queue and ack first — a slow handler reads as a timeout.
  • Return 2xx on duplicates. A re-delivery of a known event id still needs a 2xx: a fast duplicate ack stops the retry cadence.
  • Handle receiver exceptions. A 500 from your handler is the same retry trigger as a routing failure. Catch and log the exception, run the side effect on your worker queue, and still ack 2xx once the event is persisted — the durable consumer pattern covers the shape.

Signature path — WEBHOOK_SIGNATURE_INVALID

  • Wrong secret. Test and live secrets do not cross-verify; a masked preview (whsec_********...<last4>) is for identification, not for pasting into your verifier. If the cleartext is lost, rotate with POST /api/v1/webhooks/{id}/rotate-secret and store the new value — it is returned once.
  • Raw-body mismatch. The signed string is <t>.<raw_body>. If a body parser (Express express.json(), a global Fastify parser, Next.js bodyParser) consumed the request first, the re-serialized bytes never match — verify against the raw bytes (express.raw(), request.text()) before any parsing.
  • Rotation window. During the grace window, check every v1 candidate in X-Orbit-Signature (fall back to X-Devotel-Signature) and also X-Orbit-Signature-Next against the previous secret.
Every signature failure class — rotation headers, clock skew, mutated bodies, unsafe comparison — is expanded in Troubleshooting: signature verification failures.

See also