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.
Code
Common mistakes
Signatures never match
Signatures never match
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.It works locally but fails in production
It works locally but fails in production
Check the clock. Outside the 300-second window, even a perfect signature is rejected. Containers with drifting clocks are the usual cause.
Header not found
Header not found
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.Using == to compare
Using == to compare
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.
