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

# Troubleshooting webhooks

> Symptom → cause table for the failures we see most often.

If something's not working, work through these in order — they're sorted by
how commonly each one is the answer.

## Symptom → cause

<AccordionGroup>
  <Accordion title="Every request fails verification with 401 invalid signature">
    Three likely causes, in order of probability:

    1. **You're hashing the parsed body, not the raw body.** Most web
       frameworks parse JSON automatically and then `JSON.stringify` it back
       for you — but the bytes differ from what klikit sent (key order,
       whitespace). Capture the raw bytes *before* parsing. See the
       [reference Node receiver](/partner-api/examples/node-receiver) for the
       `express.json({ verify })` trick, or the
       [Python one](/partner-api/examples/python-receiver) for
       `request.get_data(cache=True)`.

    2. **Wrong secret.** Confirm `KLIKIT_WEBHOOK_SECRET` in your env matches
       what your operator gave you. Don't paste it through a shell that
       might strip a trailing newline.

    3. **Reverse proxy is rewriting the body.** Some WAFs / API gateways
       re-encode JSON. If you're behind one, turn off body transformation
       for the webhook path, or verify before the proxy in front of your
       app.
  </Accordion>

  <Accordion title="My handler gets called twice for the same order">
    This means your previous delivery returned non-2xx or timed out, so
    klikit retried. The retry uses the **same** `x-klikit-event-id`. Make
    your handler idempotent on that header:

    ```python theme={null}
    if seen_event_ids.contains(event_id):
        return 200, {"status": "duplicate-ignored"}
    seen_event_ids.add(event_id)
    process(payload)
    ```

    Use a persistent store (Redis SETNX, Postgres unique index) — an
    in-memory set evaporates on every restart and lets duplicates back in.
  </Accordion>

  <Accordion title="Klikit support says deliveries are failing but I don't see them in my logs">
    Klikit's webhook delivery service has a hard **10-second** timeout. If
    your endpoint is slow but eventually returns 200, klikit has already
    counted it as a timeout, given up, and queued a retry. Your access logs
    will show the 200 — but klikit's view is "delivery failed".

    **Fix:** acknowledge in `<1 second` by replying 200 first, then doing
    the actual work on a background queue. The reference receivers both do
    this via `setImmediate` (Node) or a thread (Python).
  </Accordion>

  <Accordion title="The x-klikit-* headers don't reach my app">
    Almost always a reverse-proxy or CDN stripping unknown headers. Common
    culprits:

    * **nginx** with `underscores_in_headers off` (the default) drops headers
      with underscores — but klikit uses hyphens, so check
      `proxy_pass_request_headers on` and that you aren't stripping `x-*`.
    * **Cloudflare** strips some headers in WAF rules — check Rules →
      Transform Rules.
    * **AWS API Gateway** has an explicit allow-list for forwarded headers.

    Run `tcpdump` or your proxy's access log with full headers to confirm
    klikit *is* sending them; then walk back through your stack to find
    where they get dropped.
  </Accordion>

  <Accordion title="I changed something and now klikit isn't delivering at all">
    Most likely your URL changed but the registration didn't. Webhook URLs
    are registered out-of-band by your klikit operator. If you migrated
    domains or paths, send the new URL + the brand/branch ids to
    [integrations@klikit.io](mailto:integrations@klikit.io) and we'll
    update the registration.

    To replay anything that was attempted against the old URL in the
    meantime, ask your operator to replay from `hookit.webhook_logs`.
  </Accordion>

  <Accordion title="I want to test against the dev environment without exposing my server">
    Two options:

    * **Forward your local port to a public URL** with `ngrok` or
      `cloudflared tunnel`, and give your klikit operator the temporary
      URL. The tunnel terminates TLS for you, and the local Node / Python
      reference receiver runs as-is.
    * **Run the reference receiver locally** and replay captured deliveries
      with `curl` (see [Verify signatures](/partner-api/webhooks/verify) for
      the recipe). This validates the verifier without needing klikit to
      reach you.
  </Accordion>
</AccordionGroup>

## Still stuck?

Open a ticket with [integrations@klikit.io](mailto:integrations@klikit.io)
and include:

* The `x-klikit-event-id` of a failing delivery (we can look it up in
  `hookit.webhook_logs`).
* The HTTP status your endpoint returned, or whether the delivery timed
  out.
* A small reproducer if you have one — even pseudo-code helps.
