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

Symptom → cause

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 for the express.json({ verify }) trick, or the Python one 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.
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:
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.
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).
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.
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 [email protected] 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.
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 for the recipe). This validates the verifier without needing klikit to reach you.

Still stuck?

Open a ticket with [email protected] 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.