> ## Documentation Index
> Fetch the complete documentation index at: https://docs.amps.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# How to verify webhook signatures

> Every Amps webhook carries a signature. Verify with the SDK or HMAC SHA-256, reject signatures older than 5 minutes, dedupe on the svix-id header.

## Webhook Signature Verification

Every Amps webhook carries a Svix signature header. Verify it before processing the payload to confirm the request originated from Amps and has not been tampered with.

<Warning>
  Never process webhooks without verifying signatures. Unverified webhooks could be malicious requests.
</Warning>

## Signature Header

Every webhook carries three Svix headers, and signature verification uses all three:

```
svix-id: msg_2a3b4c5d6e7f8g9h
svix-timestamp: 1640995200
svix-signature: v1,g0hQad8xK3qE1l2m...
```

`svix-signature` is a space-separated list of `v1,<base64>` signatures (a second entry appears briefly after a secret rotation, so a valid request matches any one of them). `svix-timestamp` is the Unix second the message was signed, used for replay protection. The scheme is the standard Svix HMAC SHA-256.

## Verifying Signatures

### Using Svix SDK (Recommended)

The easiest way to verify webhooks is using the Svix SDK:

```javascript theme={null}
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 your language has no Svix SDK, replicate the scheme by hand. The signed content is `{svix-id}.{svix-timestamp}.{rawBody}`, the HMAC key is the base64 body of your `whsec_...` secret, and the result is a base64-encoded HMAC SHA-256 digest:

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhook(rawBody, headers, secret) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature']; // space-separated "v1,<base64>" list

  if (!id || !timestamp || !signatureHeader) {
    return false;
  }

  // Replay protection: reject timestamps more than 5 minutes from now.
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return false;
  }

  // The secret is "whsec_<base64>"; the HMAC key is the decoded base64 body.
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signedContent = `${id}.${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', key)
    .update(signedContent)
    .digest('base64');
  const expectedBuf = Buffer.from(expected);

  // Several signatures can appear after a secret rotation; match any one.
  return signatureHeader.split(' ').some((entry) => {
    const sig = entry.split(',')[1]; // strip the "v1," version prefix
    if (!sig) {
      return false;
    }
    const sigBuf = Buffer.from(sig);
    // Length-guard first: timingSafeEqual throws on a length mismatch.
    return (
      sigBuf.length === expectedBuf.length &&
      crypto.timingSafeEqual(sigBuf, expectedBuf)
    );
  });
}
```

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

<Warning>
  Never commit your webhook secret to version control. Store it securely and rotate it if exposed.
</Warning>

## Security Best Practices

<AccordionGroup>
  <Accordion title="Always Verify Signatures">
    Every webhook request must be verified before processing. Reject any request without a valid signature.
  </Accordion>

  <Accordion title="Check Timestamps">
    Verify that webhook timestamps are recent (within 5 minutes) to prevent replay attacks.
  </Accordion>

  <Accordion title="Use HTTPS">
    Always use HTTPS endpoints for webhooks. Never accept webhooks over HTTP in production.
  </Accordion>

  <Accordion title="Store Secrets Securely">
    Use environment variables or secret management services. Never hardcode secrets.
  </Accordion>

  <Accordion title="Implement Idempotency">
    Use the `svix-id` header to prevent processing duplicate webhooks. Store processed delivery IDs.
  </Accordion>

  <Accordion title="Rate Limiting">
    Implement rate limiting on your webhook endpoint to prevent abuse.
  </Accordion>
</AccordionGroup>

## Example Implementation

### Node.js/Express

```javascript theme={null}
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, req.headers);

    // Check idempotency. The per-delivery ID is the svix-id header.
    const deliveryId = req.headers['svix-id'];
    if (processedEvents.has(deliveryId)) {
      return res.status(200).send('Already processed');
    }

    // Process the flat body
    handleWebhook(verified);

    // Mark as processed
    processedEvents.add(deliveryId);

    res.status(200).send('OK');
  } catch (err) {
    console.error('Webhook verification failed:', err);
    res.status(400).send('Invalid signature');
  }
});

function handleWebhook(webhook) {
  // The body is flat: read fields like webhook.command or webhook.deviceId.
  console.log('Received webhook for device:', webhook.deviceId);
  // Your webhook processing logic here
}
```

### Python/Flask

```python theme={null}
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, request.headers)

        # Check idempotency. The per-delivery ID is the svix-id header.
        delivery_id = request.headers.get('svix-id')
        if delivery_id in processed_events:
            return jsonify({'status': 'Already processed'}), 200

        # Process the flat body
        handle_webhook(verified)

        # Mark as processed
        processed_events.add(delivery_id)

        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):
    # The body is flat: read fields like webhook["command"] or webhook["deviceId"].
    print(f'Received webhook for device: {webhook["deviceId"]}')
    # Your webhook processing logic here
```

## Testing Webhook Verification

### Local Testing

Use tools like [ngrok](https://ngrok.com) to expose your local endpoint:

```bash theme={null}
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

<AccordionGroup>
  <Accordion title="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
  </Accordion>

  <Accordion title="Getting duplicate webhooks">
    Implement idempotency checking using the `svix-id` header. Store processed delivery IDs and skip duplicates.
  </Accordion>

  <Accordion title="Webhooks timing out">
    Ensure your endpoint responds within 30 seconds. Process webhooks asynchronously if needed.
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhook Types" icon="list" href="/reference/webhook-types">
    See all webhook event types
  </Card>

  <Card title="Webhook Overview" icon="webhook" href="/guides/webhooks/receive">
    Learn about webhook setup
  </Card>
</CardGroup>
