Overview
Webhook signature verification is a security mechanism that proves a webhook request was sent by JsonHook (and not by an attacker who knows your endpoint URL). Without it, anyone who discovers your webhook URL can POST arbitrary payloads that your handler will process as legitimate email deliveries.
JsonHook uses HMAC-SHA256 signatures. When an inbound address is created, a shared secret is generated and returned in the API response. JsonHook uses this secret to compute an HMAC-SHA256 digest of the raw request body for every delivery, and includes it in the X-JsonHook-Signature request header.
Your webhook handler must:
- Read the raw request body before any parsing
- Compute
HMAC-SHA256(secret, rawBody) - Compare the result to the
X-JsonHook-Signatureheader value - Reject (return 401) if they do not match
- Only then parse and process the payload
Prerequisites
To implement signature verification you need:
- Your webhook secret — returned when you created the inbound address via
POST /v1/addresses. Store it as an environment variable (JSONHOOK_SECRET). - A web framework that provides access to the raw (unparsed) request body bytes — this is critical. If a JSON middleware has already parsed the body, you cannot reconstruct the exact bytes that were signed.
- A standard HMAC-SHA256 implementation — available in the standard library of every major programming language.
If you have lost your webhook secret, you can rotate it via POST /v1/addresses/{id}/rotate-secret. The new secret takes effect immediately, so update your environment variable promptly.
Secure Your Webhook Endpoint
HMAC-SHA256 signatures on every delivery. Built in on all plans.
Get Free API KeyStep-by-Step Instructions
Implement signature verification in 4 steps:
- Configure raw body access. Make sure your framework gives you the raw request body bytes before any parsing middleware runs. In Express, add a raw body parser for the webhook route. In Django, use
request.body. In Flask, userequest.data. - Read the signature header:
If the header is absent, reject the request immediately.const signature = req.headers["x-jsonhook-signature"]; - Compute the expected signature:
const expected = crypto .createHmac("sha256", process.env.JSONHOOK_SECRET) .update(rawBody) .digest("hex"); - Compare using a timing-safe function:
Use a timing-safe comparison to prevent timing oracle attacks. Standard string equality (if (!crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) )) { return res.status(401).send("Invalid signature"); }===) leaks timing information.
Code Example
Multi-language signature verification examples:
Node.js:
import crypto from "crypto";
function verifySignature(rawBody: Buffer, secret: string, sig: string): boolean {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Python:
import hmac, hashlib
def verify_signature(raw_body: bytes, secret: str, sig: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)
Go:
import ("crypto/hmac"; "crypto/sha256"; "encoding/hex")
func verifySignature(rawBody []byte, secret, sig string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
Ruby:
require "openssl"
def verify_signature(raw_body, secret, sig)
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
ActiveSupport::SecurityUtils.secure_compare(sig, expected)
endCommon Pitfalls
Signature verification frequently fails due to these mistakes:
- Body already parsed by middleware. If
express.json()or equivalent runs before your handler, the raw bytes are gone and your HMAC will not match. Configure a dedicated raw body parser for the webhook route only. - Comparing with standard string equality. Use
timingSafeEqual(Node.js),hmac.compare_digest(Python),hmac.Equal(Go), orsecure_compare(Ruby). Regular equality is vulnerable to timing attacks. - Encoding mismatch. Both the received signature and your computed signature must be hex-encoded strings before comparison. Do not mix hex and base64.
- Extra whitespace or newlines added to the body. Middleware that reformats or pretty-prints the JSON body will break the HMAC. Always verify against the exact bytes received over the wire.
- Wrong secret. Ensure you are using the secret from the specific address the delivery was sent to. If you have multiple addresses, each has its own secret.