Skip to main content
Your webhook URL is not a secret. It sits in logs, proxies, and integration tools. The signature is what proves a request came from us.

The steps

1

Take the raw body

The exact bytes we sent, before any JSON parsing.
2

Build the signed string

{timestamp}.{raw_body} — the timestamp header, a dot, then the body.
3

HMAC it

HMAC-SHA256 with your whsec_ secret, as lowercase hex.
4

Compare

Add v1= in front and compare with the header, using a timing-safe compare.
5

Check the clock

Reject anything more than 300 seconds from now.
Sign the raw body, never a re-built object. JSON.parse then JSON.stringify changes the bytes, and your signature will never match. {"a":1} and {"a": 1} give different signatures — that is the point.

Code

Common mistakes

You are almost surely signing a re-built body. express.json(), bodyParser.json(), and most framework middleware parse the request and throw the original bytes away. Get the raw body first — express.raw() in Node, request.get_data() in Flask, php://input in PHP.A trailing newline also breaks it. echo adds one; printf does not.
Check the clock. Outside the 300-second window, even a perfect signature is rejected. Containers with drifting clocks are the usual cause.
Header names ignore case in HTTP, but some frameworks rename them. PHP shows them as HTTP_X_MYAICHATBOT_SIGNATURE. Some proxies strip headers they do not know.
A plain == leaks tiny timing clues. In theory, an attacker can use them to guess a valid signature one byte at a time. Use timingSafeEqual, hmac.compare_digest, hash_equals, or hmac.Equal.

No-code platforms

n8n, Make, and Zapier cannot easily run an HMAC in a trigger step. In practice, you rely on the URL being hard to guess — so treat it like a password.
If your bot does anything sensitive — refunds, account changes, private data — put a real server in front that checks the signature, then have it call your scenario. An unchecked webhook URL is a password that never changes.

Changing the secret

Make a new secret in your bot’s webhook settings. It starts working right away, with no overlap window. Deploy the new secret first, and switch at a quiet time.