Clipform

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.

The simplest and safest option is the official standardwebhooks library, which handles parsing, replay/timestamp checks, and constant-time comparison for you:

npm install standardwebhooks
import { 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 invalid

The same library exists for other languages (Python, Go, Ruby, PHP, Rust, ...) with an identical interface.

Headers

HeaderFormatDescription
webhook-idmsg_<uuid>Unique message ID for idempotency
webhook-timestampUnix secondsWhen the payload was sent
webhook-signaturev1,<base64>HMAC-SHA256 signature

Verifying manually

If you can't use the library, the scheme is straightforward:

  1. Reject timestamps older than 5 minutes (replay protection).
  2. Construct the signed content: {webhook-id}.{webhook-timestamp}.{body}.
  3. Compute v1, + base64(HMAC-SHA256(secret, signedContent)), using your signing secret with the whsec_ prefix stripped and base64-decoded.
  4. Compare against the webhook-signature header 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));
}

On this page