Webhook signature
Learn how to verify Checkout Page webhook signatures to ensure authenticity
Webhook security is critical. All webhooks sent by Checkout Page include a signature in the x-webhook-signature header that you should verify to ensure the webhook is authentic and hasn't been tampered with.
Why verify signatures?
Verifying webhook signatures protects your application by:
- Ensuring the webhook came from Checkout Page
- Preventing request forgery and replay attacks
- Detecting if the webhook payload has been modified in transit
How it works
Checkout Page generates a signature using HMAC-SHA256 with your webhook secret. The signature is sent in the x-webhook-signature header with every webhook request in the format sha256=<hex-digest>. You can verify the signature by:
- Taking the raw webhook payload as a string
- Computing an HMAC-SHA256 hash using your webhook secret
- Comparing the computed hash with the signature in the header using constant-time comparison
The signature format is always sha256=<64-character-hex-string>. You must parse out the hex digest portion before comparison.
Important: Always use constant-time comparison (not standard string equality) to prevent timing attacks. This prevents attackers from using the time taken to compare signatures to guess the correct signature character-by-character.
Webhook headers
Every webhook request includes the following headers:
| Header | Format | Description |
|---|---|---|
x-webhook-signature | sha256=<hex> | HMAC-SHA256 signature of the payload |
x-webhook-timestamp | Unix timestamp | Time the webhook was sent |
x-webhook-delivery-attempt | Number | Attempt number (1, 2, 3, etc.) for retries |
Example x-webhook-signature header value:
sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Implementation
Node.js / Express / Koa
Here's the recommended function to verify webhook signatures:
import crypto from 'crypto';
/**
* Verify webhook signature
*/
export function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string,
): boolean {
try {
// Parse the signature header format: "sha256=<hex-digest>"
const [algorithm, receivedSignature] = signatureHeader.split('=');
// Verify algorithm is sha256
if (algorithm !== 'sha256') {
return false;
}
// Generate expected signature
const expectedSignature = generateWebhookSignature(payload, secret);
// Compare using constant-time comparison
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
const receivedBuffer = Buffer.from(receivedSignature, 'utf8');
if (expectedBuffer.length !== receivedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
} catch (error) {
return false;
}
}
/**
* Generate webhook signature
*/
export function generateWebhookSignature(payload: string, secret: string): string {
const hmac = crypto.createHmac('sha256', secret).update(payload);
return `sha256=${hmac.digest('hex')}`;
}Using with Express
import express from 'express';
import { verifyWebhookSignature } from './webhook-utils';
app.post('/webhooks/events', express.json(), (req, res) => {
const signatureHeader = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
const isValid = verifyWebhookSignature(payload, signatureHeader, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
// Process the webhook
const event = req.body;
const timestamp = req.headers['x-webhook-timestamp'];
const attempt = req.headers['x-webhook-delivery-attempt'];
console.log('Webhook received:', {
type: event.type,
id: event.id,
timestamp,
attempt,
});
res.status(200).json({ success: true });
});Best practices
Store your webhook secret securely
Never hardcode your webhook secret. Always use environment variables:
# .env file
WEBHOOK_SECRET=1234567890abcdefLog webhook events
For debugging, log incoming webhooks (but never log the signature):
console.log('Webhook received', {
event: requestBody.event,
id: requestBody.event_id,
timestamp: requestBody.timestamp,
sellerId: requestBody.seller_id,
// Do NOT log: signature, secret, or sensitive data
});Handle errors gracefully
router.post('/webhooks/events', async ctx => {
try {
const signatureHeader = ctx.headers['x-webhook-signature'];
// Verify signature exists
if (!signatureHeader) {
ctx.status = 400;
ctx.body = { error: 'Missing webhook signature' };
return;
}
const payload = JSON.stringify(ctx.request.body);
const isValid = verifyWebhookSignature(payload, signatureHeader, process.env.WEBHOOK_SECRET);
if (!isValid) {
ctx.status = 401;
ctx.body = { error: 'Invalid webhook signature' };
return;
}
// Process webhook...
ctx.status = 200;
ctx.body = { success: true };
} catch (error) {
console.error('Webhook processing error:', error);
ctx.status = 500;
ctx.body = { error: 'Internal server error' };
}
});Idempotency
Webhook deliveries may be retried. Ensure your webhook handler is idempotent by:
- Storing processed webhook IDs
- Checking if a webhook has already been processed before handling it
const processedWebhooks = new Set();
router.post('/webhooks/events', async ctx => {
// ... signature verification ...
const event = ctx.request.body;
// Check if we've already processed this webhook
if (processedWebhooks.has(event.id)) {
ctx.status = 200;
ctx.body = { success: true };
return;
}
// Process the webhook
// ... your logic here ...
processedWebhooks.add(event.id);
ctx.status = 200;
ctx.body = { success: true };
});Common issues
"Invalid webhook signature"
Cause 1: Payload is not stringified correctly
- Ensure you're using
JSON.stringify()on the raw request body - Don't parse and re-stringify - use the exact bytes received
Cause 2: Secret is incorrect or missing
- Verify
WEBHOOK_SECRETis set in your environment variables - Check that it matches the secret shown in your dashboard
Cause 3: Missing or malformed signature header
- Ensure the
x-webhook-signatureheader is present - Check for typos in the header name
- Verify the signature format is
sha256=<64-char-hex-string>
Testing webhooks
In the Checkout Page dashboard click on the Test button. This sends a request with dummy data to your webhook and can be used to verify and test your webhook endpoint.
Security considerations
- Never skip signature verification in production
- Use HTTPS for all webhook endpoints
- Rotate secrets periodically for enhanced security
- Log failures for monitoring and debugging
- Set timeouts to prevent hanging requests
- Use idempotency keys to handle retried webhooks
Next steps
Explore these webhook-related topics: