> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: webhook signature verification fails (HMAC mismatch)

> End-to-end runbook for WEBHOOK_SIGNATURE_INVALID — reproduce the HMAC mismatch locally, isolate wrong-secret/raw-body/middleware causes, read the delivery log in the dashboard, and recover with the retry semantics.

# Troubleshooting: webhook signature verification fails (HMAC mismatch)

Your endpoint answers deliveries with a 401 and the delivery log shows
`WEBHOOK_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](/troubleshooting/webhook-lifecycle-codes);
for every HMAC failure class in depth, see
[signature verification failures](/webhooks/troubleshooting-signature-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 legacy `X-Devotel-Signature` header).
* `X-Orbit-Webhook-Id` — the delivery id; log it so you can match the failed
  attempt to a dashboard row later.

Pull one failed delivery
(`GET /api/v1/webhooks/{id}/deliveries/{deliveryId}`), copy the raw payload
into a file, and recompute:

```bash theme={null}
SECRET="whsec_..."          # cleartext secret, not the masked preview
T=1737302400                # the t= value from the captured header
EXPECTED=$(printf '%s.' "$T" | cat - payload.json \
  | openssl dgst -sha256 -hmac "$SECRET")
echo "$EXPECTED"
```

Compare the digest to every `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** (or
`POST /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](/webhooks/troubleshooting-signature-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 (any `4xx`, usually `401`). 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.

Read the status your endpoint returned from the delivery row: `4xx` points
to the signature path, everything else to the delivery path. The full code
map is in
[webhook lifecycle error codes](/troubleshooting/webhook-lifecycle-codes).

## 5. Retry semantics while you fix

A `401` 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](/troubleshooting/webhook-deliveries).
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](/troubleshooting/webhook-lifecycle-codes) —
  the code-to-state map this runbook anchors.
* [Signature verification failures](/webhooks/troubleshooting-signature-failures) —
  every HMAC failure class with copy-paste verifiers.
* [Webhook security](/webhooks/security) — the signature/delivery contract.
* [Webhook events reference](/reference/webhook-events) — the event catalog
  your verifier protects.
* [Failed deliveries, retries, DLQ, and replay](/troubleshooting/webhook-deliveries) —
  the recovery loop after the fixes land.
