Skip to main content

Verify webhook signatures

We sign every webhook with:
  • Signature = a tamper-proof stamp made with your secret. If the body or timestamp changes, the stamp won’t match.
  • t (timestamp) = when we created the event (seconds since epoch).
  • v1 = the signature itself (base64 string).
Algorithm (v1): base64(HMAC_SHA256(secret, "${ts}.${raw_body}")) with a ±5 minute tolerance.
We sign the exact raw bytes you receive (no reformatting). Verify against the raw body, not a re-serialized JSON object.

What to validate (in order)

  1. Read raw body
    Use the raw bytes from the request. Don’t JSON stringify/pretty-print before verifying.
  2. Parse the header
    Expect t=<unix_ts>,v1=<base64sig>.
  3. Freshness (clock skew)
    Make sure the request is recent: abs(now - t) ≤ 300s (5 min).
    Why: prevents old/replayed requests from being accepted.
  4. Recompute the signature
    Rebuild it yourself with your secret:
    sig = base64(HMAC_SHA256(secret, ts + "." + raw_body)).
  5. Secure compare
    Compare your sig with the header’s v1 using a constant-time function (a “secure equals”).
    Why: avoids tiny timing differences that could leak info.
  6. Replay protection (nice to have)
    Keep a short-lived cache of X-Webhook-Id (or the (t,v1) pair). If you see the same one again within ~10 minutes, reject it.
    Why: blocks attackers from re-sending a previously valid request.
  7. Then parse JSON & ack fast
    s Once verified, parse JSON and return 2xx quickly. Do heavy work async; we retry on non-2xx.
You’ll also get headers
  • X-Webhook-Event: e.g. order.submitted, order.pending, order.settled, order.failed, order.refunded
  • X-Webhook-Id: unique request id (use for replay protection)
  • X-Webhook-Signature: the header above
Headers you’ll get
  • X-Webhook-Event: e.g. order.submitted, order.processing, order.settled, order.failed, order.refunded
  • X-Webhook-Id: unique id for idempotency
  • X-Webhook-Signature: the signature header above

Minimal receivers (drop-in)

Node (Express)

Python (FastAPI)


Sample payload (event body)


Local testing

  • Best: use your own signer to hit your local receiver.
    • Node: compute v1 with crypto.createHmac("sha256", secret).update(ts.{ts}.).digest("base64").
    • Python: compute v1 with base64(hmac_sha256(secret, f"{ts}.{raw_body}")).
  • Or call our POST /webhooks/test to validate your signature logic (no secrets in the docs UI).

Common errors