Documentation

Webhook signature verification

Verify the mr-signature header to ensure webhook requests came from ModelRiver. Prevent spoofing with HMAC-SHA256 validation.

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

  1. ModelRiver computes an HMAC-SHA256 hash of the payload using your webhook secret
  2. The hash is sent in the mr-signature header
  3. Your backend computes the same hash and compares the values
  4. If they match, the request is authentic

Node.js implementation

JAVASCRIPT
1const crypto = require('crypto');
2 
3function verifyWebhookSignature(payload, signature, secret) {
4 const expectedSignature = crypto
5 .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 webhook
24 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 hmac
2import hashlib
3import json
4from flask import Flask, request, jsonify
5 
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.sha256
11 ).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'}), 401
22
23 # Signature verified: process the webhook
24 payload = request.json
25 print(f'Verified webhook: {payload["data"]}')
26
27 return jsonify({'received': True}), 200

Security best practices

  1. Always verify signatures: Never process webhooks without validating the mr-signature header
  2. Use timing-safe comparison: Use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python) to prevent timing attacks
  3. Use HTTPS endpoints: ModelRiver only sends webhooks to https:// URLs in production
  4. Implement idempotency: Use channel_id to deduplicate webhook deliveries
  5. Set reasonable timeouts: Respond to webhooks within 10 seconds; use background jobs for long-running tasks
  6. Store secrets securely: Keep webhook signature secrets in environment variables, never in code

Next steps