Skip to main content

Webhook Signature Verification

Intro

Every webhook request includes an X-Khaime-Signature header — an HMAC-SHA256 hash of the raw request body, signed with your webhook secret. Always verify it before acting on a webhook, so a spoofed request to your endpoint can’t trigger real side effects (granting access, marking an order paid, releasing inventory).

Context

The signature covers the exact bytes Khaime sent, using the same webhook secret you set alongside your webhook URL (see Webhooks Overview). Verification is the gate that should run before any of the best practices — idempotency checks, event handling — touch the payload. If verification fails, reject with 401 and stop; don’t fall through to processing “just in case.”

Hows

  1. Khaime computes HMAC-SHA256(webhook_secret, raw_request_body)
  2. Sends the hex digest in X-Khaime-Signature
  3. You recompute the same hash using the raw request body and compare
Use the raw request body (not a re-serialized version) for signature verification. Re-serializing JSON can change key ordering or whitespace, causing verification to fail.

Implementation

Use constant-time comparison (timingSafeEqual, compare_digest, hash_equals) to prevent timing attacks. Never use === or == for signature comparison.
If your webhook secret is ever exposed, regenerate it from Khaime Dashboard → Settings → API (or the equivalent API key management endpoint) — regenerating invalidates the old secret immediately, so every request signed with it will fail verification going forward.

Whys

Verification is HMAC over the raw body — not a full request-signing scheme like mutual TLS — because it’s the simplest mechanism every partner stack (any language, any framework) can implement correctly with a standard library and no extra infrastructure. Requiring the raw, unparsed body (rather than a canonicalized JSON form) removes an entire class of “verification works in dev but breaks in production” bugs caused by middleware or a JSON library subtly reformatting the body before your handler sees it — that’s why every code sample above reads the body as raw bytes/text before anything else touches it. Constant-time comparison matters because a naive === check on a hex string leaks timing information proportional to how many leading characters match, which an attacker can exploit over enough requests to reconstruct a valid signature without ever knowing the secret.

Why nots

Signature verification confirms the request came from Khaime and the body hasn’t been tampered with in transit — it does not confirm the event hasn’t been processed before (that’s what the event_id idempotency check in Best Practices is for), and it does not confirm the event is fresh (there’s no timestamp-based replay window enforced on the receiving side, so pair signature checks with your own idempotency store rather than assuming a valid signature means “new event”). Don’t compare signatures with ==/===, don’t skip verification for “internal” or “low-risk” event types, and don’t verify against a re-serialized copy of the payload — any of these silently reduces verification to a no-op.