Getting Started

Vouch sends webhook events to the URL you configure in Settings → Webhooks. Each request is a POST with a JSON body and an x-vouch-signature header you should verify before trusting the payload. See the Webhooks section of the API reference for the exact shape of each event’s payload.

Verifying the signature

Vouch signs every webhook request with HMAC-SHA256, keyed by the webhook secret shown when you create the webhook. The signature is sent in the x-vouch-signature header in the form:

t=<unix timestamp>,v1=<hex hmac of "{t}.{raw body}">

Verify it by recomputing the HMAC over {timestamp}.{raw request body} using your webhook secret, and comparing it to v1 with a constant-time comparison. Reject requests where the timestamp is more than 5 minutes old, to guard against replay attacks.

Example (Node.js)

const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => {
const [k, ...rest] = part.split('=');
return [k.trim(), rest.join('=').trim()];
})
);
const timestamp = Number(parts.t);
const expected = parts.v1;
if (!timestamp || !expected) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) {
return false;
}
const signedPayload = `${timestamp}.${rawBody}`;
const signature = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch {
// Mismatched length (e.g. a malformed or tampered v1 value) throws rather
// than returning false — treat that the same as "doesn't match".
return false;
}
}
// Usage — rawBody MUST be the exact raw request body string/bytes you
// received, never a value you've re-JSON.stringify'd after parsing it —
// key ordering and whitespace aren't guaranteed to round-trip identically
// to what was actually signed, so a re-serialized body can fail verification
// even for a legitimate request. If you're on Express with express.json(),
// capture the raw body via a `verify` callback (or express.raw()) before
// the JSON parser consumes it — req.body is not the same bytes that were signed.
const isValid = verifyWebhookSignature(
request.rawBody,
request.headers['x-vouch-signature'],
process.env.WEBHOOK_SECRET
);

Important: Always use crypto.timingSafeEqual (or an equivalent constant-time comparison) when verifying signatures, to prevent timing attacks — and always wrap it in a try/catch, since it throws (rather than returning false) when the two buffers differ in length.