Verify webhook signatures
Verify Slovio webhook signatures: check the X-Slovio-Signature HMAC-SHA256 header so you know a webhook really came from Slovio, with Node.js and Python code.
2 min readUpdated
When a webhook has a Signing secret, every delivery carries two headers:
X-Slovio-Timestamp: 1790838612
X-Slovio-Signature: t=1790838612,v1=5f2b7c…The signature is an HMAC-SHA256, keyed with your signing secret, of the timestamp, a dot, and the raw request body:
v1 = hex( HMAC_SHA256( secret, "<timestamp>.<raw body>" ) )Verify
- Read the raw body before any JSON parsing.
- Compute the HMAC and compare it to
v1in constant time. - Reject timestamps more than 5 minutes old, to stop replays.
Node.js
import crypto from "node:crypto";
export function verifySlovio(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Python
import hmac, hashlib, time
def verify_slovio(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts.get("t", 0))) > 300:
return False
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Common questions
Where do I find my signing secret?
For messaging webhooks, it is the Signing secret you set on the webhook. For calling webhooks, it is shown once when you add the endpoint; use Rotate secret to get a new one.
Why does my signature never match?
Most often the body was parsed and re-serialised before hashing. Hash the raw bytes exactly as received, and include the timestamp and the dot: <t>.<raw body>.
Is the signature the same for calling webhooks?
Yes. Calling webhooks use the same t=<unix seconds>,v1=<hex> header and the same HMAC-SHA256 of <t>.<raw body>.
Why reject old timestamps?
So a captured request can't be replayed later. Five minutes allows for normal clock differences.
Stuck, or is something here out of date? Tell the team — we reply within a working day.