Skip to main content

Signature verification fails

Your endpoint is receiving requests, but Webhook-Signature never matches what you compute. Failure modes:
The signature is computed over the raw request body bytes; any middleware that parses and re-serializes the JSON breaks the HMAC.Symptoms
  • 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.0 becoming 2 between the wire and your handler.
FixCapture the raw body before any JSON parsing, and verify against those exact bytes - the per-framework instructions are on the verify page. Verify first, parse second.
Order matters within a single handler too. If you call req.json() (or your framework’s equivalent) before grabbing the raw bytes, the raw body is already gone.
The signing secret is shown only once, when you provision it. Common ways this goes wrong:
  • 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.
FixYou cannot re-read the existing secret. Rotate it, capture the returned value, and deploy it to your verifier. A 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.
Applies to from-scratch verifiers only - the official 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.
FixDerive the key per the reference implementation, or switch to an official library.
Applies to from-scratch verifiers only. The signed content is {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.
FixBuild the signed content exactly as in the reference implementation - verbatim parts, no transforms.
Applies to from-scratch verifiers only. The Webhook-Signature header is not a bare signature. It looks like:
v1,g0hM9S…base64…
And the spec allows it to carry multiple space-delimited signatures:
v1,g0hM9S…  v1,bX8kQ2…
Symptoms
  • You string-compared the entire header against your computed base64 and it never matched (the v1, prefix alone breaks it).
FixSplit the header on whitespace, take only the 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.
The timestamp-tolerance check is enforced on your side - if your server’s clock has drifted, a perfectly valid, recently-signed delivery gets rejected even though the HMAC matches.Symptoms
  • 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.
Fix
  • Correct the receiver clock rather than widening or removing the timestamp check.
  • Webhook-Timestamp is Unix epoch seconds, not milliseconds - comparing it against Date.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.
1

Check the subscription status

Fetch the subscription and read its status. The watcher must be live before anything fires.
Get one subscription
curl "https://api.walletsuite.io/api/notifications/subscriptions/{id}" \
  -H "x-api-key: $WALLETSUITE_API_KEY"
Deliveries flow only while the status is 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.
2

Confirm the URL still points at your infrastructure

URL requirements are validated at create time - an existing subscription already passed them. What can break afterwards:
  • 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 3xx counts as a failure and is not followed. Point the subscription at the final URL directly.
3

Check what your endpoint returned

Your endpoint’s access logs tell you which side of the acknowledgement contract failed:
  • 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.
  • 5xx or timeout - the handler crashed or processed synchronously past the window - see respond before you process.
4

Verify the watched chain and address match exactly

Events only fire for the exact (chain, address) you subscribed. Mismatches are silent - no error, just no events.
  • chain matches the chain the transfer actually happened on (e.g. "ethereum", "tron").
  • The watched address matches the on-chain to address 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 returns 409 SUBSCRIPTION_EXISTS rather than a second watcher.
List your subscriptions to confirm what’s actually registered:
List subscriptions (newest first)
curl "https://api.walletsuite.io/api/notifications/subscriptions?limit=100" \
  -H "x-api-key: $WALLETSUITE_API_KEY"
5

Check whether the event was abandoned after exhausting retries

If your endpoint failed every attempt across the full retry window, that event was abandoned. Back-fill the gap by re-querying chain state for the watched address (for example via the REST transaction-history endpoints) - the chain and the API always hold the authoritative record.
6

Confirm the transfer matches your asset scope

If you subscribed with only ["INCOMING_NATIVE"], an incoming token transfer will not fire - add "INCOMING_FUNGIBLE" (see event kinds vs event types).
To generate a real event for debugging, send a small transfer to a watched address. See the quickstart for the full create-subscribe-receive flow.

FAQ

Deliveries are retried across the full retry window; after the final attempt the event is permanently abandoned. The per-attempt schedule and total span are on the Delivery & retries page.
Ordering is best-effort, not guaranteed - process each event independently and use occurred_at for sequence. See Delivery semantics.
No - deliveries retry automatically on the retry schedule. Reconcile missed events by re-querying chain state for the watched address via the REST API.
On standard plans, delivery IPs are dynamic - authenticate with signature verification. Static egress IPs per regional environment are available on Enterprise plans.
Retries keep delivering for the full retry window, so a brief outage loses nothing once the endpoint recovers. A persistent outage additionally triggers automatic pausing; the subscription resumes automatically after a cooldown and re-arms any held deliveries. Events that arrive while it is paused are not delivered, so back-fill them by re-querying chain state via the REST API.

Verify signatures

The full Standard Webhooks signing scheme, plus the per-framework raw-body fix table.

Delivery semantics

Retries, ordering, at-least-once delivery, and dedupe in detail.

Event payloads

The transfer.received envelope and every field’s semantics.

Best practices

Build an endpoint that acknowledges fast and never drops events.