Skip to main content

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.

Signature Header

Webhooks include the svix-signature header:
svix-signature: v1,abc123def456...,timestamp=1640995200
The signature format follows the Svix webhook signature standard.

Verifying Signatures

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:
  1. Navigate to Webhooks settings
  2. Select your webhook endpoint
  3. Copy the webhook secret
  4. 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:
ngrok http 3000
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

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
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