Verify signatures
Every notification is signed with your endpoint’s secret. Verifying the signature is what tells you that Hablame sent it and that nobody changed it on the way.
4 min read
How it is computed
- 01Take the time exactly as it arrives
It is the text of the
X-Hablame-Timestampheader: Unix seconds, unconverted. - 02Build the signed message
The time, a dot and the raw body, byte for byte:
"<timestamp>.. Do not parse it as JSON and write it back: a single extra space and the signature no longer matches.<body>" - 03Compute the HMAC-SHA256
Using your full signing secret as the key,
whsec_included. Encode it as lowercase hex and prependsha256=. - 04Compare in constant time
Against
X-Hablame-Signature. If it does not match andX-Hablame-Signature-Previs present, compare that one too. If neither matches, respond401and do not process the notification.
Never compare with =
A regular comparison stops at the first different character, and the time it takes tells an attacker how many they got right. Use your language’s constant-time function: timingSafeEqual, hmac., hash_equals, hmac., MessageDigest. or CryptographicOperations..
Check the time
The time is part of the signature so that a captured notification is not valid forever. Accept only notifications whose time is an integer within 300 seconds of your clock, and keep your clock in sync. A legitimate retry never arrives with an old time: the time and the signature are recalculated on every attempt, over the same body.
Test your code with this example
This notification is signed with an example secret. If your function, given that secret, that time and the exact body of the example request, produces the expected signature, your computation is correct. Skip the time check for this test: the example has a fixed date.
| Item | Value |
|---|---|
| Example secret (not a real one) | whsec_TU_SECRETO_DE_FIRMA |
X-Hablame-Timestamp | 1792854247 |
| Body | The one in the example request, on a single line |
Expected X-Hablame-Signature | sha256= |
# It must print the same hex as the expected signature, without the sha256= prefix
printf '%s' '1792854247.{"id":"0192f0c47b1e7a3d9c553e2a1b4c5d6e","type":"sms.mo.87654","data":{"shortCode":"87654","from":"573001234567","text":"Hola, quiero más información","receivedAt":"2026-10-24T15:04:05Z","messageId":"0192f0c4-7b1e-7a3d-9c55-3e2a1b4c5d6e"}}' \
| openssl dgst -sha256 -hmac 'whsec_TU_SECRETO_DE_FIRMA'When the secret is rotated
During the 24 hours after a rotation, each notification carries two signatures with the same time and body: X-Hablame-Signature with the new secret and X-Hablame-Signature-Prev with the previous one. If your code compares against both, you can switch the secret in your system at any point in that window without rejecting a single notification. After the window, only the new signature arrives.
Common mistakes
whsec_.whsec_ prefix from the secret.= = or equals.X-Hablame-Signature-Prev.