Verify a webhook signature
Prove a delivery came from SUQO before acting on it.
Always verify before acting on a payload. Anyone can POST to your URL; the signature is what proves the request came from SUQO.
The signature
HMAC-SHA256( key = your signing secret,
message = "<X-SUQO-Timestamp>" + "." + <raw request body> )hex-encoded and prefixed with sha256=. It arrives in X-SUQO-Signature, alongside X-SUQO-Timestamp — see Request format for the full header set.
Two rules that matter:
- Use the raw request body bytes — exactly as received. Do not parse to JSON and re-serialize; key order and whitespace would change and the signature would not match.
- Compare in constant time (
hmac.compare_digest,crypto.timingSafeEqual) — not==.
The timestamp is part of the signed message, so a captured request cannot be replayed under a new time. Reject requests whose timestamp is more than ~5 minutes old.
Store the signing secret as a server-side secret — never in frontend code or a public repo.
Python
import hashlib
import hmac
def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Node
const crypto = require("crypto")
function verify(rawBody, signature, timestamp, secret) {
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody) // Buffer — raw bytes
.digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(signature)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Common signature problems
| Symptom | Likely cause |
|---|---|
| Signature never matches | Body was parsed and re-serialized before verifying — use the raw bytes. In Express, a JSON body parser mounted before your route is the usual culprit. |
| Matches locally, fails in production | Wrong secret in that environment, or a proxy/WAF rewriting the body. |
| Requests rejected as too old | Server clock drift on your side. Sync via NTP. |
Security: Treat the signing secret as a password. Never commit it to source control, expose it in client-side code, or paste it into screenshots or support tickets.