Webhook Signature Verification
All webhooks from Amps AI include a signature header that you must verify to ensure the request is authentic and hasn’t been tampered with.
Never process webhooks without verifying signatures. Unverified webhooks could be malicious requests.
Webhooks include the svix-signature header:
svix-signature: v1,abc123def456...,timestamp=1640995200
The signature format follows the Svix webhook signature standard.
Verifying Signatures
Using Svix SDK (Recommended)
The easiest way to verify webhooks is using the Svix SDK:
import { Webhook } from 'svix';
const webhookSecret = process.env.WEBHOOK_SECRET; // From your dashboard
const svix = new Webhook(webhookSecret);
app.post('/webhooks', (req, res) => {
const signature = req.headers['svix-signature'];
const payload = JSON.stringify(req.body);
try {
const verified = svix.verify(payload, signature);
// Process verified webhook
handleWebhook(verified);
res.status(200).send('OK');
} catch (err) {
res.status(400).send('Invalid signature');
}
});
Manual Verification
If you need to verify manually, the signature is an HMAC SHA256 hash:
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const elements = signature.split(',');
const signatureHash = elements.find(el => el.startsWith('v1='));
const timestamp = elements.find(el => el.startsWith('timestamp='));
if (!signatureHash || !timestamp) {
return false;
}
const sig = signatureHash.split('=')[1];
const ts = timestamp.split('=')[1];
// Check timestamp (prevent replay attacks)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(ts)) > 300) { // 5 minutes
return false;
}
// Verify signature
const signedPayload = `${ts}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expectedSig)
);
}
Getting Your Webhook Secret
Your webhook secret is available in the Amps AI Dashboard:
- Navigate to Webhooks settings
- Select your webhook endpoint
- Copy the webhook secret
- Store it securely (environment variable, secret manager)
Never commit your webhook secret to version control. Store it securely and rotate it if exposed.
Security Best Practices
Every webhook request must be verified before processing. Reject any request without a valid signature.
Verify that webhook timestamps are recent (within 5 minutes) to prevent replay attacks.
Always use HTTPS endpoints for webhooks. Never accept webhooks over HTTP in production.
Use environment variables or secret management services. Never hardcode secrets.
Use eventId to prevent processing duplicate webhooks. Store processed event IDs.
Implement rate limiting on your webhook endpoint to prevent abuse.
Example Implementation
Node.js/Express
const express = require('express');
const { Webhook } = require('svix');
const app = express();
const webhookSecret = process.env.WEBHOOK_SECRET;
const svix = new Webhook(webhookSecret);
// Store processed event IDs (use Redis in production)
const processedEvents = new Set();
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['svix-signature'];
if (!signature) {
return res.status(400).send('Missing signature');
}
try {
const payload = req.body.toString();
const verified = svix.verify(payload, signature);
// Check idempotency
if (processedEvents.has(verified.eventId)) {
return res.status(200).send('Already processed');
}
// Process webhook
handleWebhook(verified);
// Mark as processed
processedEvents.add(verified.eventId);
res.status(200).send('OK');
} catch (err) {
console.error('Webhook verification failed:', err);
res.status(400).send('Invalid signature');
}
});
function handleWebhook(webhook) {
console.log('Received webhook:', webhook.event);
// Your webhook processing logic here
}
Python/Flask
from flask import Flask, request, jsonify
from svix import Webhook
import os
app = Flask(__name__)
webhook_secret = os.environ.get('WEBHOOK_SECRET')
svix = Webhook(webhook_secret)
processed_events = set()
@app.route('/webhooks', methods=['POST'])
def webhook():
signature = request.headers.get('svix-signature')
if not signature:
return jsonify({'error': 'Missing signature'}), 400
try:
payload = request.get_data(as_text=True)
verified = svix.verify(payload, signature)
# Check idempotency
if verified['eventId'] in processed_events:
return jsonify({'status': 'Already processed'}), 200
# Process webhook
handle_webhook(verified)
# Mark as processed
processed_events.add(verified['eventId'])
return jsonify({'status': 'OK'}), 200
except Exception as e:
print(f'Webhook verification failed: {e}')
return jsonify({'error': 'Invalid signature'}), 400
def handle_webhook(webhook):
print(f'Received webhook: {webhook["event"]}')
# Your webhook processing logic here
Testing Webhook Verification
Local Testing
Use tools like ngrok to expose your local endpoint:
Then configure the ngrok URL in your dashboard for testing.
Signature Testing
Test your verification logic with known good signatures before going to production.
Troubleshooting
Signature verification always fails
Check that:
- You’re using the correct webhook secret
- The payload hasn’t been modified (use raw body parser)
- The signature header is being read correctly
- Timestamps are within the acceptable window
Getting duplicate webhooks
Implement idempotency checking using eventId. Store processed event IDs and skip duplicates.
Ensure your endpoint responds within 30 seconds. Process webhooks asynchronously if needed.
Next Steps
Webhook Types
See all webhook event types
Webhook Overview
Learn about webhook setup