Skip to main content
Verifying webhook signatures is crucial for security. It ensures that the webhook actually came from storekit and hasn’t been tampered with.

Why Verify?

Since your webhook endpoint is publicly accessible, anyone could send requests to it pretending to be storekit. Signature verification prevents:
  • Spoofed requests: Attackers sending fake webhooks
  • Replay attacks: Old webhooks being resent maliciously
  • Data tampering: Modification of webhook payloads in transit
We recommend using the official Svix libraries for verification, which handle all the complexity for you:
import { Webhook } from 'svix';

const wh = new Webhook(process.env.WEBHOOK_SECRET);

try {
  const payload = wh.verify(rawBody, headers);
  // Process the verified payload
} catch (err) {
  // Signature verification failed
  return res.status(400).send('Invalid signature');
}
Always use the raw request body for verification. If you parse the JSON first and then stringify it, the signature will not match due to potential formatting differences.

Manual Verification

If you prefer to verify signatures manually without using the Svix library, follow the steps below.

Signature Headers

Each webhook includes these headers for verification:
HeaderDescription
svix-idUnique message identifier
svix-timestampUnix timestamp of when the message was sent
svix-signatureThe signature(s) to verify against

Verification Steps

1. Extract the Headers

const svixId = request.headers['svix-id'];
const svixTimestamp = request.headers['svix-timestamp'];
const svixSignature = request.headers['svix-signature'];

2. Verify Timestamp (Prevent Replay Attacks)

Reject webhooks with timestamps older than 5 minutes:
const tolerance = 5 * 60; // 5 minutes in seconds
const now = Math.floor(Date.now() / 1000);
const timestamp = parseInt(svixTimestamp, 10);

if (Math.abs(now - timestamp) > tolerance) {
  throw new Error('Webhook timestamp too old');
}

3. Create the Signed Content

Concatenate the webhook ID, timestamp, and body:
const signedContent = `${svixId}.${svixTimestamp}.${rawBody}`;

4. Calculate Expected Signature

Use HMAC-SHA256 with your webhook secret:
const crypto = require('crypto');

// Your secret from the dashboard (remove the 'whsec_' prefix)
const secret = Buffer.from(secretKey.split('_')[1], 'base64');

const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(signedContent)
  .digest('base64');

5. Compare Signatures

const signatures = svixSignature.split(' ');
const isValid = signatures.some(sig => {
  const [version, signature] = sig.split(',');
  return version === 'v1' && signature === expectedSignature;
});

if (!isValid) {
  throw new Error('Invalid webhook signature');
}