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

# Troubleshooting Webhooks

> Resolution checklists for signature verification and delivery, ordered by frequency, with an FAQ.

## Signature verification fails

Your endpoint is receiving requests, but `Webhook-Signature` never matches what you compute. Failure modes:

<AccordionGroup>
  <Accordion title="1. Your body was mutated before you verified it" icon="wand-magic-sparkles">
    The signature is computed over the [raw request body bytes](/webhooks/verify#verify-against-the-raw-body); 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.

    **Fix**

    Capture the raw body *before* any JSON parsing, and verify against those exact bytes - the [per-framework instructions](/webhooks/verify#verify-against-the-raw-body) are on the verify page. Verify first, parse second.

    <Warning>
      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.
    </Warning>
  </Accordion>

  <Accordion title="2. You're using the wrong secret" icon="key">
    The signing secret is [shown only once, when you provision it](/webhooks/quickstart). 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.

    **Fix**

    You cannot re-read the existing secret. [Rotate it](/webhooks/verify#secret-rotation), 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](/webhooks/quickstart) first.
  </Accordion>

  <Accordion title="3. You used the whsec_ string as the HMAC key" icon="scissors">
    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.

    **Fix**

    Derive the key per the [reference implementation](/webhooks/verify#verify-manually), or switch to an official library.
  </Accordion>

  <Accordion title="4. Your signed string is constructed wrong" icon="hashtag">
    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.

    **Fix**

    Build the signed content exactly as in the [reference implementation](/webhooks/verify#verify-manually) - verbatim parts, no transforms.
  </Accordion>

  <Accordion title="5. You compared the whole header instead of parsing the v1 entries" icon="code-compare">
    Applies to from-scratch verifiers only. The `Webhook-Signature` header is **not** a bare signature. It looks like:

    ```text theme={null}
    v1,g0hM9S…base64…
    ```

    And the spec allows it to carry **multiple space-delimited signatures**:

    ```text theme={null}
    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).

    **Fix**

    Split 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](/webhooks/verify#verify-manually).
  </Accordion>

  <Accordion title="6. Clock skew is breaking your timestamp tolerance check" icon="clock">
    The [timestamp-tolerance check](/webhooks/verify#replay-protection) 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](/webhooks/verify#verify-manually).
  </Accordion>
</AccordionGroup>

## Deliveries aren't arriving

Your verifier is fine, but events never reach your endpoint. Work through these in order.

<Steps>
  <Step title="Check the subscription status">
    Fetch the subscription and read its `status`. The watcher must be live before anything fires.

    ```bash Get one subscription theme={null}
    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](/webhooks/delivery#subscription-status-lifecycle). A `404 SUBSCRIPTION_NOT_FOUND` means the id is wrong or the subscription was deleted.
  </Step>

  <Step title="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](/webhooks/delivery#acknowledgement) and is not followed. Point the subscription at the final URL directly.
  </Step>

  <Step title="Check what your endpoint returned">
    Your endpoint's access logs tell you which side of the [acknowledgement contract](/webhooks/delivery#acknowledgement) 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](/webhooks/verify), 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](/webhooks/best-practices).
  </Step>

  <Step title="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](/webhooks/events#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:

    ```bash List subscriptions (newest first) theme={null}
    curl "https://api.walletsuite.io/api/notifications/subscriptions?limit=100" \
      -H "x-api-key: $WALLETSUITE_API_KEY"
    ```
  </Step>

  <Step title="Check whether the event was abandoned after exhausting retries">
    If your endpoint failed every attempt across the full [retry window](/webhooks/delivery#retry-behavior), 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.
  </Step>

  <Step title="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](/webhooks/events#event-kinds-vs-event-types)).
  </Step>
</Steps>

<Tip>
  To generate a real event for debugging, send a small transfer to a watched address. See the [quickstart](/webhooks/quickstart) for the full create-subscribe-receive flow.
</Tip>

## FAQ

<AccordionGroup>
  <Accordion title="How long do retries last?">
    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](/webhooks/delivery#retry-behavior) page.
  </Accordion>

  <Accordion title="Are deliveries ordered?">
    Ordering is **best-effort, not guaranteed** - process each event independently and use `occurred_at` for sequence. See [Delivery semantics](/webhooks/delivery#delivery-semantics).
  </Accordion>

  <Accordion title="Can I get an event redelivered?">
    No - deliveries retry automatically on the [retry schedule](/webhooks/delivery#retry-behavior). Reconcile missed events by re-querying chain state for the watched address via the REST API.
  </Accordion>

  <Accordion title="Can I allowlist your IPs?">
    On standard plans, delivery IPs are dynamic - authenticate with [signature verification](/webhooks/verify). [Static egress IPs](/webhooks/delivery#egress-ips) per regional environment are available on Enterprise plans.
  </Accordion>

  <Accordion title="What happens if my endpoint is down for a day?">
    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](/webhooks/delivery#subscription-status-lifecycle); 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.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Verify signatures" icon="shield-check" href="/webhooks/verify">
    The full Standard Webhooks signing scheme, plus the per-framework raw-body fix table.
  </Card>

  <Card title="Delivery semantics" icon="truck-fast" href="/webhooks/delivery">
    Retries, ordering, at-least-once delivery, and dedupe in detail.
  </Card>

  <Card title="Event payloads" icon="brackets-curly" href="/webhooks/events">
    The `transfer.received` envelope and every field's semantics.
  </Card>

  <Card title="Best practices" icon="list-check" href="/webhooks/best-practices">
    Build an endpoint that acknowledges fast and never drops events.
  </Card>
</CardGroup>
