This guide shows you how to add the Billing & Invoice Assistant template to your own application, step by step. No prior AI experience is needed - you will copy code, not write AI logic.
What this does
Triages billing questions - invoices, failed payments, plan changes - and drafts a clear resolution reply.
The template ships with 2 workflows and 2 response formats that already work together:
- Workflows:
billing_triage_workflow,billing_resolution_workflow - Response formats:
billing_triage_response,billing_resolution_response
Prefer a fully no-code path? Skip ahead to Step 6 - Go no-code.
What you will build
- A backend endpoint that starts the Billing & Invoice Assistant workflow with the customer's request.
- A real-time chat UI that streams the AI reply as it is generated.
- A webhook receiver that gets the pipeline event and sends the result back to the customer.
Step 1 - Download and import the template
- Open the Billing & Invoice Assistant template page and click Download JSON.
- In your ModelRiver project, click Import, paste or upload the file, review the preview, and confirm. Everything is created atomically - nothing is overwritten.
- Learn more in the import & export guide.
Step 2 - Connect the AI providers
This template runs on OpenAI and Anthropic. Connect them in the Providers section of your project before importing. If you use different providers, you can change them on any workflow after import.
Step 3 - Add the backend endpoint
Your backend starts the workflow by calling the ModelRiver API with a project API key (create one in Project settings → API keys). The response includes a one-time ws_token that your frontend uses to stream the reply.
// Express.js - POST /api/ai/chatimport express from 'express';const app = express();app.use(express.json()); app.post('/api/ai/chat', async (req, res) => { const response = await fetch('https://api.modelriver.com/v1/ai/async', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MR_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ workflow: 'billing_triage_workflow', messages: [{ role: 'user', content: req.body.message }], inputs: { customer_id: req.body.customer_id }, }), }); const data = await response.json(); // Return the one-time ws_token so the frontend can stream the reply. res.json({ ws_token: data.ws_token, channel_id: data.channel_id });}); app.listen(3000);Run this endpoint locally with the CLI for a no-hassle start: modelriver listen forwards ModelRiver webhooks to your localhost.
Step 4 - Stream the reply in your frontend
Use the ModelRiver client SDK to connect to the stream. When you call connect() with the ws_token from your backend, the AI reply arrives in real time - no polling.
import { useState } from 'react';import { useModelRiver } from '@modelriver/client/react'; export function TemplateChat() { const [input, setInput] = useState(''); const { connect, response, error, status } = useModelRiver({ baseUrl: 'wss://api.modelriver.com/socket', persist: true, }); const handleSend = async () => { // 1. Your backend starts the billing_triage_workflow workflow and returns a one-time ws_token. const res = await fetch('/api/ai/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: input }), }); const { ws_token, channel_id } = await res.json(); // 2. Connect to ModelRiver's stream. The reply arrives in real time. connect({ wsToken: ws_token, channelId: channel_id }); setInput(''); }; return ( <div> <input value={input} onChange={(e) => setInput(e.target.value)} disabled={status === 'loading'} placeholder="Type your request..." /> <button onClick={handleSend} disabled={!input || status === 'loading'}>Send</button> {status === 'loading' && <p>Working on it...</p>} {response && ( <div> <p>{response.content}</p> {response.data && <pre>{JSON.stringify(response.data, null, 2)}</pre>} </div> )} {error && <p>{error}</p>} </div> );}Install the SDK with npm install @modelriver/client. The same ws_token works with React, Vue, Vanilla JS, and Svelte.
Step 5 - Receive the pipeline event in your backend
When the AI step finishes, ModelRiver delivers a task.ai_generated event to the webhook URL configured in your project. Your backend can add its own data (your policy, order details, or database lookups) and then call the callback_url from the event. The final reply then streams to the customer.
The signature is verified with the mr-signature header (HMAC-SHA256 of the raw body using your webhook secret).
// Express.js - POST /webhooks/modelriverimport express from 'express';import crypto from 'node:crypto'; const app = express();app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } })); app.post('/webhooks/modelriver', async (req, res) => { // 1. Verify the HMAC signature. const signature = req.headers['mr-signature'] as string; const secret = process.env.MR_WEBHOOK_SECRET; const expected = crypto .createHmac('sha256', secret) .update((req as any).rawBody) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(401).send('Invalid signature'); } const event = req.body; // 2. The AI step finished. Enrich it with your own data (order, policy, ...). const enriched = { ...event.ai_response, checked_at: new Date().toISOString() }; await saveEvent(event.channel_id, enriched); // your database // 3. Send the enriched result back so the reply streams to the customer. if (event.callback_url && event.callback_required) { await fetch(event.callback_url, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MR_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ data: enriched }), }); } res.status(200).send('ok');}); app.listen(3000);Step 6 - Go no-code
Prefer not to run a server? These platforms can receive the pipeline event, store it, and call back - all without managing infrastructure.
// Supabase Edge Function - webhooks/modelriver// Deploy with: supabase functions deploy webhooks-modelriverimport { serve } from 'https://deno.land/[email protected]/http/server.ts';import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!); serve(async (req) => { const event = await req.json(); // 1. Store the AI step result in Postgres. const { error } = await supabase.from('ai_content').insert({ channel_id: event.channel_id, payload: event.ai_response, status: 'ai_generated', }); if (error) return new Response(error.message, { status: 500 }); // 2. Send the result back so the reply streams to the customer. if (event.callback_url && event.callback_required) { await fetch(event.callback_url, { method: 'POST', headers: { Authorization: `Bearer ${Deno.env.get('MR_API_KEY')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ data: event.ai_response }), }); } return new Response('ok');});Test it end to end
- Start your backend (or deploy your no-code function) and set
MR_API_KEY,MR_WEBHOOK_SECRET, and the webhook URL in your project. - Open your frontend, type a test request, and press Send.
- Confirm the reply streams in real time and that your backend received the
task.ai_generatedevent.