Documentation

Standard webhooks

For workflows without an event name, ModelRiver sends the complete AI response to your webhook endpoint immediately after processing.

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:

JSON
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": 0
32 },
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": 1767616496
42}
FieldDescription
channel_idUnique identifier for this request. Use it to deduplicate deliveries
timestampUnix time in seconds when this delivery attempt was signed
callback_urlAlways present. Only used by backend pipeline workflows; for a standard workflow, calling it returns 404 channel_not_found
data.status"success" or "error"
data.dataThe AI result. With a structured output, this is the validated object
data.customer_dataThe cache fields of the request, keyed by field path
data.metaProvider 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:

JSON
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

HeaderDescription
x-modelriver-signatureHMAC-SHA256 signature. See signature verification
x-modelriver-timestampUnix time in seconds when this delivery attempt was signed
x-modelriver-webhook-idID of the webhook endpoint this delivery was sent to
content-typeAlways application/json

Integration examples

Verify the signature against the raw request body first, then read the event from data.

Node.js (Express)

JAVASCRIPT
1// verifyModelRiverSignature: see /docs/webhooks/signature-verification
2app.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_SECRET
9 );
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 here
16 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)

PYTHON
1# verify_modelriver_signature: see /docs/webhooks/signature-verification
2@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'}), 401
12 
13 event = json.loads(raw_body)['data']
14 
15 if event['status'] == 'success':
16 # Your business logic here
17 print(f"AI completed: {event['data']}")
18 else:
19 print(f"AI request failed: {event['error']}")
20 
21 return jsonify({'received': True}), 200

Next steps