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.
Never process webhooks without verifying signatures. Unverified webhooks could be malicious requests.
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.
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:
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) ); });}
from flask import Flask, request, jsonifyfrom svix import Webhookimport osapp = 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'}), 400def 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