How standard webhooks work
For workflows without an event_name, ModelRiver sends the complete AI response to your webhook endpoint immediately after processing. This is a simple fire-and-forget pattern: your backend receives the result and can process it as needed. No callback is required; respond with any 2xx status.
Webhooks are sent for asynchronous requests (POST /v1/ai/async) only, to every enabled webhook endpoint in the project. Synchronous requests and OpenAI-compatible calls return their result directly and do not send webhooks.
Webhook payload structure
Every delivery is an envelope. The event is inside data:
1{2 "callback_url": "https://api.modelriver.com/v1/callback/550e8400-e29b-41d4-a716-446655440000",3 "channel_id": "550e8400-e29b-41d4-a716-446655440000",4 "data": {5 "channel_id": "550e8400-e29b-41d4-a716-446655440000",6 "customer_data": {7 "metadata.ticket_id": "T-123"8 },9 "data": {10 "summary": "Customer requested refund for order #12345...",11 "sentiment": "negative",12 "category": "billing"13 },14 "message": "success",15 "meta": {16 "status": "success",17 "http_status": 200,18 "workflow": "customer_support_summary",19 "requested_provider": "openai",20 "requested_model": "gpt-5.6-terra",21 "used_provider": "openai",22 "used_model": "gpt-5.6-terra",23 "duration_ms": 2341,24 "usage": {25 "prompt_tokens": 245,26 "completion_tokens": 89,27 "total_tokens": 334,28 "cache_read_tokens": 0,29 "cache_write_tokens": 0,30 "cache_write_1h_tokens": 0,31 "reasoning_tokens": 032 },33 "structured_output": true,34 "attempts": [35 { "provider": "openai", "model": "gpt-5.6-terra", "status": "success" }36 ],37 "customer_fields": ["metadata.ticket_id"]38 },39 "status": "success"40 },41 "timestamp": 176761649642}| Field | Description |
|---|---|
channel_id | Unique identifier for this request. Use it to deduplicate deliveries |
timestamp | Unix time in seconds when this delivery attempt was signed |
callback_url | Always present. Only used by backend pipeline workflows; for a standard workflow, calling it returns 404 channel_not_found |
data.status | "success" or "error" |
data.data | The AI result. With a structured output, this is the validated object |
data.customer_data | The cache fields of the request, keyed by field path |
data.meta | Provider and model actually used, token usage, duration, and each attempt (including failovers) |
Error deliveries
If the request fails, for example because every model in the workflow failed or a spending limit was reached, the webhook is still sent. data.status is "error" and data.error describes what happened:
1{2 "channel_id": "550e8400-e29b-41d4-a716-446655440000",3 "status": "error",4 "message": "Model is no longer available",5 "error": {6 "type": "model_unavailable",7 "message": "Model is no longer available",8 "workflow": "customer_support_summary",9 "attempts": [ ... ]10 },11 "customer_data": { "metadata.ticket_id": "T-123" },12 "meta": {13 "status": "error",14 "http_status": 422,15 "workflow": "customer_support_summary",16 "duration_ms": 512,17 "usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "cache_read_tokens": 0, "cache_write_tokens": 0, "cache_write_1h_tokens": 0, "reasoning_tokens": 0 },18 "attempts": [ ... ]19 }20}Headers sent with webhooks
| Header | Description |
|---|---|
x-modelriver-signature | HMAC-SHA256 signature. See signature verification |
x-modelriver-timestamp | Unix time in seconds when this delivery attempt was signed |
x-modelriver-webhook-id | ID of the webhook endpoint this delivery was sent to |
content-type | Always application/json |
Integration examples
Verify the signature against the raw request body first, then read the event from data.
Node.js (Express)
1// verifyModelRiverSignature: see /docs/webhooks/signature-verification2app.post('/webhooks/ai', express.raw({ type: 'application/json' }), (req, res) => {3 const rawBody = req.body.toString('utf8');4 const ok = verifyModelRiverSignature(5 rawBody,6 req.get('x-modelriver-timestamp'),7 req.get('x-modelriver-signature'),8 process.env.MODELRIVER_WEBHOOK_SECRET9 );10 if (!ok) return res.status(401).json({ error: 'Invalid signature' });11 12 const event = JSON.parse(rawBody).data;13 14 if (event.status === 'success') {15 // Your business logic here16 console.log('AI completed:', event.data, event.customer_data);17 } else {18 console.error('AI request failed:', event.error);19 }20 21 res.status(200).json({ received: true });22});Python (Flask)
1# verify_modelriver_signature: see /docs/webhooks/signature-verification2@app.route('/webhooks/ai', methods=['POST'])3def handle_webhook():4 raw_body = request.get_data()5 if not verify_modelriver_signature(6 raw_body,7 request.headers.get('x-modelriver-timestamp', ''),8 request.headers.get('x-modelriver-signature', ''),9 os.environ['MODELRIVER_WEBHOOK_SECRET'],10 ):11 return jsonify({'error': 'Invalid signature'}), 40112 13 event = json.loads(raw_body)['data']14 15 if event['status'] == 'success':16 # Your business logic here17 print(f"AI completed: {event['data']}")18 else:19 print(f"AI request failed: {event['error']}")20 21 return jsonify({'received': True}), 200Next steps
- Signature verification: Verify webhook authenticity
- Backend pipeline workflows: Add custom processing before final delivery
- Delivery & retries: Understand retry policies