Troubleshooting: webhook signature verification fails (HMAC mismatch)
Your endpoint answers deliveries with a 401 and the delivery log showsWEBHOOK_SIGNATURE_INVALID. That means Orbit delivered and your verifier
rejected it — the HMAC your server computed matched none of the v1
candidates in the signature header. This page walks the full recovery loop:
confirm the mismatch, isolate the cause, fix it, and let the retries recover
the backlog.
For the array-of-codes view, see
webhook lifecycle error codes;
for every HMAC failure class in depth, see
signature verification failures.
1. Reproduce the mismatch locally
Recreate the exact comparison Orbit expects before you change anything. A delivery carries two headers that drive verification:X-Orbit-Signature—t=<unix_seconds>,v1=<hex hmac>(integrating since before the rename? fall back to the legacyX-Devotel-Signatureheader).X-Orbit-Webhook-Id— the delivery id; log it so you can match the failed attempt to a dashboard row later.
GET /api/v1/webhooks/{id}/deliveries/{deliveryId}), copy the raw payload
into a file, and recompute:
v1= candidate in the captured header. If none
match, the failure is real and one of the causes in the next section applies.
Any one match means your runtime code — not the secret — is dropping the raw
body or a candidate header.
2. The three causes, in order of frequency
Wrong secret after rotation. Test and live secrets do not cross-verify, and a rotation in Developer → Webhooks (orPOST /api/v1/webhooks/{id}/rotate-secret) invalidates the old value the
moment your deploys pick up. The masked preview
(whsec_********...<last4>) identifies the secret — pasting it into your
verifier guarantees a mismatch. If the cleartext is lost, rotate again and
store the new value; it is returned exactly once.
Payload re-serialization. The signed string is <t>.<raw_body> — the
exact bytes Orbit POSTed. If a body parser (Express express.json(), a
global Fastify content parser, Next.js bodyParser) ran first, key order,
whitespace, and Unicode escaping change and the recomputed HMAC can never
match. Verify against raw bytes (express.raw(), request.text()) before
any parsing.
Middleware drops the raw body (or the header). Logging proxies,
compression middleware, or a framework that buffers only parsed JSON leave
your verifier with nothing to sign — or a proxy may strip the
X-Orbit-Signature header itself. Capture req.rawBody (Express) or the
undecoded request stream and check the headers your handler actually saw.
The verifier shape that handles all three — multi-candidate parsing, raw
body, timing-safe compare — is copied in
signature verification failures.
3. Debug in the dashboard delivery log
Open Developer → Webhooks → Delivery log and find the failing row. The row is redacted — secrets and signature values never appear — but it shows the response status your endpoint returned (401 here), the delivery id in
X-Orbit-Webhook-Id, and the attempt timestamp. Drill into the row to
compare the exact payload against what your endpoint logged; a byte-level
diff over the body usually exposes the re-serialization within a glance.
If the dashboard shows 401 on every attempt while your own logs show
200, a proxy or framework middleware is rewriting the response — confirm
the status from the receiver side before touching the verifier.
4. WEBHOOK_SIGNATURE_INVALID vs WEBHOOK_DELIVERY_FAILED
Both land in the delivery log as failed attempts, but they are different gates:WEBHOOK_SIGNATURE_INVALID— your endpoint answered and rejected the HMAC (any4xx, usually401). Fix the verifier; retries recover alone.WEBHOOK_DELIVERY_FAILED— the attempt never reached a working receiver (404,5xx, timeout, connection refused). Fix routing/uptime; the signature is not involved.
4xx points
to the signature path, everything else to the delivery path. The full code
map is in
webhook lifecycle error codes.
5. Retry semantics while you fix
A401 counts as a failed attempt, so Orbit retries on the standard cadence:
up to 9 retries with exponential backoff over roughly 4.3 hours. That gives
you a window to deploy the verifier fix — remaining retries succeed on their
own, and you do not need a bulk replay for rows that are still retrying.
If the window closes before the fix lands, the rows move to the DLQ; requeue
or replay them once the endpoint verifies cleanly. Requeue, replay, and bulk
replay-range tooling are covered in
failed webhook deliveries, retries, DLQ, and replay.
During a rotation grace window, accept every v1 candidate from
X-Orbit-Signature (and X-Orbit-Signature-Next for the previous secret) so
in-flight retries keep succeeding while you roll the new secret out.
See also
- Webhook lifecycle error codes — the code-to-state map this runbook anchors.
- Signature verification failures — every HMAC failure class with copy-paste verifiers.
- Webhook security — the signature/delivery contract.
- Webhook events reference — the event catalog your verifier protects.
- Failed deliveries, retries, DLQ, and replay — the recovery loop after the fixes land.