WebhooksSignature Verification

Signature Verification

Every webhook request carries an HMAC-SHA256 signature in X-Signature. The scheme is identical in spirit to Stripe’s: the server signs {timestamp}.{raw_body} so an attacker cannot replay an old body under a new timestamp.

Algorithm

X-Signature = lowercase_hex(
  HMAC-SHA256(
    key  = your_webhook_secret,         // bytes of the secret string (UTF-8)
    data = X-Timestamp + "." + raw_body // ASCII dot, no spaces
  )
)
  • your_webhook_secret is the value shown in the merchant cabinet (Settings → Webhook → Regenerate secret). The secret is shown once — store it in your secrets manager.
  • X-Timestamp is Unix seconds, sent as a decimal string (e.g. 1746442800).
  • raw_body is the exact bytes of the request body, before any parsing or re-encoding. Do not parse and re-serialize the JSON; whitespace differences will invalidate the signature.

Verification recipe

Reject a request when any of these is true:

  1. X-Signature missing or fails constant-time comparison.
  2. X-Timestamp missing, malformed, or skewed by more than ±5 minutes (300 s).
  3. X-Event-Id already processed (dedup contract).

Always compare signatures in constant time (crypto.timingSafeEqual in Node, hmac.compare_digest in Python). String equality leaks timing.

Working code

Drop-in receivers for Node.js and Python are in Examples and Examples → Python. The verifier section there matches this algorithm exactly.

Testing your verifier

There is no published test vector for v1 — the canonical way to validate your implementation is to:

  1. Configure your webhook URL to point at a tunnel (ngrok, Cloudflare Tunnel) into your local environment.
  2. Trigger a test event by buying a low-priced item via POST /api/v1/merchant/buy, or wait for a real balance.deposited after a top-up.
  3. Capture the headers and the raw request body, run them through your verifier, and confirm the signature matches.

A common bug is to verify against the parsed JSON instead of the raw bytes — the re-serialized form will differ in whitespace or key ordering and the signature will fail. Always pull the body from a “raw” body parser hook.

Rotating the secret

Rotate from the merchant cabinet at merchant.tradeon.market in Settings → Webhook → Regenerate secret. The new secret is shown once in the UI; the old secret is invalidated immediately on save. Plan a brief window where both secrets are accepted on your side if you rotate during peak traffic.