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_secretis the value shown in the merchant cabinet (Settings → Webhook → Regenerate secret). The secret is shown once — store it in your secrets manager.X-Timestampis Unix seconds, sent as a decimal string (e.g.1746442800).raw_bodyis 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:
X-Signaturemissing or fails constant-time comparison.X-Timestampmissing, malformed, or skewed by more than ±5 minutes (300 s).X-Event-Idalready 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:
- Configure your webhook URL to point at a tunnel (ngrok, Cloudflare Tunnel) into your local environment.
- Trigger a test event by buying a low-priced item via
POST /api/v1/merchant/buy, or wait for a realbalance.depositedafter a top-up. - 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.