Signature Verification
Verify webhook signatures to ensure payloads are authentic.
Every webhook is signed per the Standard Webhooks spec. Always verify the signature in production so you know a payload genuinely came from Clipform.
Recommended: the Standard Webhooks library
The simplest and safest option is the official standardwebhooks library, which handles parsing, replay/timestamp checks, and constant-time comparison for you:
npm install standardwebhooksimport { Webhook } from "standardwebhooks";
// Your endpoint's signing secret (starts with whsec_)
const wh = new Webhook(signingSecret);
// `body` must be the RAW request body string, not a re-serialized object
const payload = wh.verify(body, {
"webhook-id": req.headers["webhook-id"],
"webhook-timestamp": req.headers["webhook-timestamp"],
"webhook-signature": req.headers["webhook-signature"],
});
// `payload` is the verified event - verify() throws if the signature is invalidThe same library exists for other languages (Python, Go, Ruby, PHP, Rust, ...) with an identical interface.
Headers
| Header | Format | Description |
|---|---|---|
webhook-id | msg_<uuid> | Unique message ID for idempotency |
webhook-timestamp | Unix seconds | When the payload was sent |
webhook-signature | v1,<base64> | HMAC-SHA256 signature |
Verifying manually
If you can't use the library, the scheme is straightforward:
- Reject timestamps older than 5 minutes (replay protection).
- Construct the signed content:
{webhook-id}.{webhook-timestamp}.{body}. - Compute
v1,+ base64(HMAC-SHA256(secret, signedContent)), using your signing secret with thewhsec_prefix stripped and base64-decoded. - Compare against the
webhook-signatureheader in constant time.
const crypto = require('crypto');
function verifyWebhook(payload, headers, secret) {
const msgId = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signature = headers['webhook-signature'];
// Replay protection
if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp)) > 300) {
throw new Error('Timestamp too old');
}
const rawSecret = Buffer.from(secret.replace('whsec_', ''), 'base64');
const signedContent = `${msgId}.${timestamp}.${payload}`;
const expected =
'v1,' +
crypto.createHmac('sha256', rawSecret).update(signedContent).digest('base64');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}