Verify Webhook Signatures
Every request carries a Vocci-Signature header, signed with the secret shown on the endpoint's edit screen. Reject any request whose signature does not match.
Vocci-Signature: t=1760000000,v1=5257a869…
tis the Unix time, in seconds, at which the request was signed.v1is the lowercase hex HMAC-SHA256 of<t>.<raw request body>, keyed with the whole secret string.
Steps
- Read the raw request body before parsing it. Re-serialized JSON will not match.
- Split the header into
tandv1. - Compute
HMAC-SHA256(secret, t + "." + rawBody)as hex. - Compare it with
v1in constant time. - Reject the request if
tis more than 5 minutes from your clock. Each attempt, including a retry, is signed afresh, so a retry is never stale.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return (
expected.length === parts.v1.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}
Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(item.split("=", 1) for item in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Zapier and similar tools
Catch Hook triggers do not check signatures on their own. Keep the URL secret, since it is the credential, and use Send test event to see that the zap receives the fields. If you need signature checks, put a small code step first, or receive the webhook in your own service.
Changing the secret
Open the endpoint, replace the secret, and update your receiver at the same time. Deliveries in flight use the new secret from the moment you save.