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

Signature Headers

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

Verification Steps

1. Extract the Headers

const webhookId = request.headers['webhook-id'];
const webhookTimestamp = request.headers['webhook-timestamp'];
const webhookSignature = request.headers['webhook-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(webhookTimestamp, 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 = `${webhookId}.${webhookTimestamp}.${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 = webhookSignature.split(' ');
const isValid = signatures.some(sig => {
  const [version, signature] = sig.split(',');
  return version === 'v1' && signature === expectedSignature;
});

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

Using Svix Libraries

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.