How HMAC validation works
Anyone who learns your webhook URL could send a fakePOST to it. HMAC Signature verification stops that: Formable and your server share a secret that only the two of you know. On every delivery, Formable uses that secret to stamp the request body, and you check the stamp before trusting the event.
Think of it like a wax seal on a letter:
- Formable takes the exact request body and your signing secret, and runs them through a one-way function (HMAC-SHA256). The result is a short fingerprint.
- That fingerprint is sent in the
Content-Sha256header. - Your server repeats the same calculation with the same secret and the raw body you received.
- If your fingerprint matches the header, the body came from Formable and wasn’t altered. If it doesn’t match, reject the request.
Your signing secret is provided as a base64-encoded string. Decode it to raw bytes and use those bytes as the HMAC key.
1
Read the raw body
Capture the request body exactly as received, before any JSON parsing or re-serialization. Re-serializing can change whitespace or key order and break verification.
2
Compute the expected signature
HMAC-SHA256 the raw body using the base64-decoded secret as the key, then base64-encode the digest.
3
Compare
Compare your computed value with the
Content-Sha256 header using a constant-time comparison. If they match, the request is authentic.Examples
Common pitfalls
Using the parsed body instead of the raw body
Using the parsed body instead of the raw body
Frameworks that auto-parse JSON discard the original bytes. Re-serializing the parsed object almost always produces a slightly different string (whitespace, key order), so the HMAC won’t match. Always sign over the raw request body.
Forgetting to base64-decode the secret
Forgetting to base64-decode the secret
The secret is a base64 string. Use its decoded bytes as the HMAC key — don’t pass the base64 string directly.
Non-constant-time comparison
Non-constant-time comparison
Compare signatures with a constant-time function (
crypto.timingSafeEqual, hmac.compare_digest, hash_equals) to avoid timing attacks.