Why verify signatures?
Webhook endpoints are publicly accessible URLs. Without signature verification, anyone could send fake payloads to your endpoint. ModelRiver signs every webhook payload with HMAC-SHA256 so you can verify authenticity.
How it works
- ModelRiver computes an HMAC-SHA256 hash of the payload using your webhook secret
- The hash is sent in the
mr-signatureheader - Your backend computes the same hash and compares the values
- If they match, the request is authentic
Node.js implementation
JAVASCRIPT
1const crypto = require('crypto');2 3function verifyWebhookSignature(payload, signature, secret) {4 const expectedSignature = crypto5 .createHmac('sha256', secret)6 .update(JSON.stringify(payload))7 .digest('hex');8 9 return crypto.timingSafeEqual(10 Buffer.from(signature),11 Buffer.from(expectedSignature)12 );13}14 15app.post('/webhooks/ai', (req, res) => {16 const signature = req.headers['mr-signature'];17 const webhookSecret = process.env.MODELRIVER_WEBHOOK_SECRET;18 19 if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {20 return res.status(401).json({ error: 'Invalid signature' });21 }22 23 // Signature verified: process the webhook24 const { type, data, customer_data } = req.body;25 console.log('Verified webhook:', data);26 27 res.status(200).json({ received: true });28});Python implementation
PYTHON
1import hmac2import hashlib3import json4from flask import Flask, request, jsonify5 6def verify_webhook_signature(payload, signature, secret):7 expected_signature = hmac.new(8 secret.encode('utf-8'),9 json.dumps(payload).encode('utf-8'),10 hashlib.sha25611 ).hexdigest()12 13 return hmac.compare_digest(signature, expected_signature)14 15@app.route('/webhooks/ai', methods=['POST'])16def handle_webhook():17 signature = request.headers.get('mr-signature')18 webhook_secret = os.environ['MODELRIVER_WEBHOOK_SECRET']19 20 if not verify_webhook_signature(request.json, signature, webhook_secret):21 return jsonify({'error': 'Invalid signature'}), 40122 23 # Signature verified: process the webhook24 payload = request.json25 print(f'Verified webhook: {payload["data"]}')26 27 return jsonify({'received': True}), 200Security best practices
- Always verify signatures: Never process webhooks without validating the
mr-signatureheader - Use timing-safe comparison: Use
crypto.timingSafeEqual(Node.js) orhmac.compare_digest(Python) to prevent timing attacks - Use HTTPS endpoints: ModelRiver only sends webhooks to
https://URLs in production - Implement idempotency: Use
channel_idto deduplicate webhook deliveries - Set reasonable timeouts: Respond to webhooks within 10 seconds; use background jobs for long-running tasks
- Store secrets securely: Keep webhook signature secrets in environment variables, never in code
Next steps
- Delivery & retries: Understand retry policies and monitoring
- Standard webhooks: Payload structure reference
- CLI tool: Test webhook signatures locally