Skip to main content
Esc
Browse docs

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:

http
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:

text
v1 = hex( HMAC_SHA256( secret, "<timestamp>.<raw body>" ) )

Verify

  1. Read the raw body before any JSON parsing.
  2. Compute the HMAC and compare it to v1 in constant time.
  3. Reject timestamps more than 5 minutes old, to stop replays.

Node.js

javascript
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

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.