Verify webhook signatures
Copy-paste signature checks in Node, Python and PHP.
Every delivery is signed. Check the signature before you trust a request. The rules are in Verifying signatures; this guide gives working code.
The signed string is:
event_id + "." + timestamp + "." + raw_body
where event_id is the X-Imploy-Event-Id header, timestamp is X-Imploy-Timestamp, and raw_body is the body exactly as received. The HMAC-SHA256 of that string, as hex and prefixed with sha256=, must equal X-Imploy-Signature.
import crypto from "node:crypto";
export function isValid(req, rawBody, secret) {
const eventId = req.header("X-Imploy-Event-Id");
const timestamp = req.header("X-Imploy-Timestamp");
const signature = req.header("X-Imploy-Signature") ?? "";
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${eventId}.${timestamp}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib
import hmac
def is_valid(headers, raw_body: bytes, secret: str) -> bool:
event_id = headers.get("X-Imploy-Event-Id", "")
timestamp = headers.get("X-Imploy-Timestamp", "")
signature = headers.get("X-Imploy-Signature", "")
message = f"{event_id}.{timestamp}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
function isValid(array $headers, string $rawBody, string $secret): bool
{
$eventId = $headers['X-Imploy-Event-Id'] ?? '';
$timestamp = $headers['X-Imploy-Timestamp'] ?? '';
$signature = $headers['X-Imploy-Signature'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', "$eventId.$timestamp.$rawBody", $secret);
return hash_equals($expected, $signature);
}
Common mistakes
- Parsing the body first. Re-encoding JSON changes the bytes. Verify against the raw body, then parse.
- Wrong secret. Use the webhook's signing secret, not your access token or the optional secret header. You can reveal it on the webhook in Settings.
- Normal comparison. Use a constant time compare, as above.
- After rotating. Rotating changes the signing secret straight away. Update your endpoint at the same time.
Reject old requests
The timestamp is in the signed string, so it can't be changed without breaking the signature. Reject a delivery whose timestamp is more than a few minutes old, so a captured request can't be replayed later.
Return 401 for a bad signature and don't process the body.