Signature verification fails
Your endpoint is receiving requests, butWebhook-Signature never matches what you compute. Failure modes:
1. Your body was mutated before you verified it
1. Your body was mutated before you verified it
- Signature fails on every request, with no exceptions.
- The decoded payload looks completely correct when you log it.
- Re-ordered keys, changed whitespace, or
2.0becoming2between the wire and your handler.
2. You're using the wrong secret
2. You're using the wrong secret
- You verify with a secret captured before a rotation. After a rotate, deliveries are signed with the new secret only - the old one will never match again.
- The deployed value is wrong - truncated, or taken from a different environment.
409 SIGNING_SECRET_EXISTS on the provision endpoint means a secret already exists - rotate instead of provisioning. A 404 SIGNING_SECRET_NOT_FOUND on rotate means you never provisioned one - provision first.3. You used the whsec_ string as the HMAC key
3. You used the whsec_ string as the HMAC key
standardwebhooks libraries take the whsec_… string as-is and handle key derivation internally. The secret is not the HMAC key: strip the whsec_ prefix, then base64-decode the remainder - the decoded bytes are the HMAC-SHA256 key.Symptoms- Verification fails uniformly, but everything else (raw body, signed string) looks right.
- You passed the full
whsec_…string straight into your HMAC function.
4. Your signed string is constructed wrong
4. Your signed string is constructed wrong
{Webhook-Id}.{Webhook-Timestamp}.{raw body} - all three parts taken verbatim from the request, joined with literal dots.Symptoms- You joined with
:or a newline instead of.. - You used a re-serialized body, or a timestamp you generated yourself instead of the header value.
- You URL-decoded, trimmed, or otherwise “cleaned” any of the three parts.
5. You compared the whole header instead of parsing the v1 entries
5. You compared the whole header instead of parsing the v1 entries
Webhook-Signature header is not a bare signature. It looks like:- You string-compared the entire header against your computed base64 and it never matched (the
v1,prefix alone breaks it).
v1, entries, and compare your computed signature against each value with a constant-time comparison - accept if any matches, exactly as in the reference implementation.6. Clock skew is breaking your timestamp tolerance check
6. Clock skew is breaking your timestamp tolerance check
- The HMAC check itself passes, but your timestamp-tolerance guard rejects the request.
- Failures correlate with one host or one container, not all of them.
- Failures cluster around deployments to a new VM/region.
- Correct the receiver clock rather than widening or removing the timestamp check.
Webhook-Timestampis Unix epoch seconds, not milliseconds - comparing it againstDate.now()(ms) always exceeds the tolerance window. Convert one side, as in the reference implementation.
Deliveries aren’t arriving
Your verifier is fine, but events never reach your endpoint. Work through these in order.Check the subscription status
status. The watcher must be live before anything fires.active - every other status and its recovery path is in the status lifecycle. A 404 SUBSCRIPTION_NOT_FOUND means the id is wrong or the subscription was deleted.Confirm the URL still points at your infrastructure
- The hostname no longer resolves, or resolves to infrastructure that isn’t yours. If the URL changed, create a new subscription pointing at the current one.
- A redirect was introduced - a
3xxcounts as a failure and is not followed. Point the subscription at the final URL directly.
Check what your endpoint returned
- TLS errors - the certificate is invalid, expired, or missing its chain.
4xx- the request is being rejected before your handler runs. Webhook deliveries authenticate via the signature, not a session or API key, so the route must accept an unauthenticated signed POST.5xxor timeout - the handler crashed or processed synchronously past the window - see respond before you process.
Verify the watched chain and address match exactly
(chain, address) you subscribed. Mismatches are silent - no error, just no events.chainmatches the chain the transfer actually happened on (e.g."ethereum","tron").- The watched address matches the on-chain
toaddress exactly. Encoding is not the issue - addresses are normalized at create time (see Address encodings) - so check for a wrong or mistyped address. - There is one active subscription per
(chain, address)pair per API key. A second create attempt with the same key returns409 SUBSCRIPTION_EXISTSrather than a second watcher.
Check whether the event was abandoned after exhausting retries
Confirm the transfer matches your asset scope
["INCOMING_NATIVE"], an incoming token transfer will not fire - add "INCOMING_FUNGIBLE" (see event kinds vs event types).FAQ
How long do retries last?
How long do retries last?
Are deliveries ordered?
Are deliveries ordered?
occurred_at for sequence. See Delivery semantics.Can I get an event redelivered?
Can I get an event redelivered?
Can I allowlist your IPs?
Can I allowlist your IPs?
What happens if my endpoint is down for a day?
What happens if my endpoint is down for a day?
Related
Verify signatures
Delivery semantics
Event payloads
transfer.received envelope and every field’s semantics.