Documentation

Backend pipeline with Next.js

Receive AI-generated webhooks in Next.js API routes, process with custom logic, and call back to ModelRiver: complete App Router implementation.

Overview

Next.js API routes (App Router) provide the ideal handler for ModelRiver backend pipeline webhooks. You receive structured AI output, run server-side logic, and call back: all within the same Next.js application that serves your frontend.

What you'll build:

  • A webhook endpoint that receives AI-generated data from ModelRiver
  • Signature verification middleware for secure webhook handling
  • Background processing with callback to ModelRiver
  • A React frontend that receives the final result via WebSocket

Quick start

Install dependencies

Bash
npx create-next-app@latest my-ai-app --typescript --app
cd my-ai-app
npm install crypto

Environment variables

Bash
# .env.local
MODELRIVER_API_KEY=mr_live_YOUR_API_KEY
MODELRIVER_WEBHOOK_SECRET=your_webhook_secret

Webhook handler

API route

TYPESCRIPT
1// app/api/webhooks/modelriver/route.ts
2import { NextRequest, NextResponse } from "next/server";
3import crypto from "crypto";
4 
5function verifySignature(
6 rawBody: string,
7 timestamp: string,
8 signature: string,
9 secret: string
10): boolean {
11 // Signed string: "<timestamp>.<exact JSON text of the raw body's "data" value>" (never re-encode)
12 const start = rawBody.indexOf('"data":');
13 const end = rawBody.lastIndexOf(',"timestamp":');
14 if (start === -1 || end === -1) return false;
15 
16 const expected = crypto
17 .createHmac("sha256", secret)
18 .update(`${timestamp}.${rawBody.slice(start + '"data":'.length, end)}`)
19 .digest("hex");
20 
21 const a = Buffer.from(signature);
22 const b = Buffer.from(expected);
23 return a.length === b.length && crypto.timingSafeEqual(a, b);
24}
25 
26export async function POST(request: NextRequest) {
27 const signature = request.headers.get("x-modelriver-signature");
28 const timestamp = request.headers.get("x-modelriver-timestamp") ?? "";
29 const rawBody = await request.text(); // raw body: needed for signature verification
30 
31 // 1. Verify webhook signature
32 if (!signature || !verifySignature(rawBody, timestamp, signature, process.env.MODELRIVER_WEBHOOK_SECRET!)) {
33 return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
34 }
35 
36 const payload = JSON.parse(rawBody).data; // the event payload is under "data"
37 const { type, event, ai_response, callback_url, customer_data } = payload;
38 
39 // 2. Check if this is a backend pipeline workflow
40 if (type === "task.ai_generated" && callback_url) {
41 // Respond immediately: process in background
42 // Note: Next.js doesn't support true background tasks in serverless,
43 // so we use waitUntil or process before responding
44 await processAndCallback(event, ai_response, callback_url, customer_data);
45 
46 return NextResponse.json({ received: true });
47 }
48 
49 // Standard webhook (no event name)
50 console.log("Standard webhook:", payload);
51 return NextResponse.json({ received: true });
52}
53 
54async function processAndCallback(
55 event: string,
56 aiResponse: { data: Record<string, unknown> },
57 callbackUrl: string,
58 customerData: Record<string, unknown>
59) {
60 try {
61 let enrichedData = { ...aiResponse.data };
62 
63 // 3. Your custom business logic based on the event
64 if (event === "content_ready") {
65 // Save to database
66 const savedRecord = await saveToDatabase(aiResponse.data);
67 enrichedData = {
68 ...enrichedData,
69 id: savedRecord.id,
70 slug: generateSlug(aiResponse.data.title as string),
71 saved_at: new Date().toISOString(),
72 };
73 }
74 
75 if (event === "review_complete") {
76 // Post to external API
77 await postToExternalService(aiResponse.data);
78 enrichedData.posted = true;
79 }
80 
81 // 4. Call back to ModelRiver with enriched data
82 const response = await fetch(callbackUrl, {
83 method: "POST",
84 headers: {
85 "Authorization": `Bearer ${process.env.MODELRIVER_API_KEY}`,
86 "Content-Type": "application/json",
87 },
88 body: JSON.stringify({
89 data: enrichedData,
90 task_id: `task_${Date.now()}`,
91 metadata: {
92 processed_at: new Date().toISOString(),
93 event,
94 },
95 }),
96 });
97 
98 if (!response.ok) {
99 throw new Error(`Callback failed: ${response.status}`);
100 }
101 
102 console.log(`✅ Callback sent for event: ${event}`);
103 } catch (error) {
104 console.error("Error processing webhook:", error);
105 
106 // Send error callback
107 await fetch(callbackUrl, {
108 method: "POST",
109 headers: {
110 "Authorization": `Bearer ${process.env.MODELRIVER_API_KEY}`,
111 "Content-Type": "application/json",
112 },
113 body: JSON.stringify({
114 data: {
115 processing_status: "failed",
116 processing_error: error instanceof Error ? error.message : "Unknown error",
117 },
118 }),
119 });
120 }
121}
122 
123// Your business logic functions
124async function saveToDatabase(data: Record<string, unknown>) {
125 // Replace with your actual database logic (Prisma, Drizzle, etc.)
126 return { id: `rec_${Date.now()}`, ...data };
127}
128 
129function generateSlug(title: string): string {
130 return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
131}
132 
133async function postToExternalService(data: Record<string, unknown>) {
134 // Replace with your actual API call
135 console.log("Posting to external service:", data);
136}

Triggering an async request

Server action

TYPESCRIPT
1// app/actions/ai.ts
2"use server";
3 
4export async function triggerContentGeneration(prompt: string) {
5 const response = await fetch("https://api.modelriver.com/v1/ai/async", {
6 method: "POST",
7 headers: {
8 "Authorization": `Bearer ${process.env.MODELRIVER_API_KEY}`,
9 "Content-Type": "application/json",
10 },
11 body: JSON.stringify({
12 model: "content_generator",
13 messages: [{ role: "user", content: prompt }],
14 }),
15 });
16 
17 const data = await response.json();
18 return {
19 channelId: data.channel_id,
20 wsToken: data.ws_token,
21 websocketUrl: data.websocket_url,
22 websocketChannel: data.websocket_channel,
23 };
24}

React component with WebSocket

TSX
1// app/components/AiContentGenerator.tsx
2"use client";
3 
4import { useState, useEffect } from "react";
5import { triggerContentGeneration } from "@/app/actions/ai";
6 
7export function AiContentGenerator() {
8 const [result, setResult] = useState<Record<string, unknown> | null>(null);
9 const [status, setStatus] = useState<"idle" | "processing" | "complete">("idle");
10 
11 async function handleGenerate(prompt: string) {
12 setStatus("processing");
13 
14 const { channelId, wsToken, websocketChannel } = await triggerContentGeneration(prompt);
15 
16 // Connect to WebSocket for real-time result
17 const ws = new WebSocket(
18 `wss://api.modelriver.com/socket/websocket?token=${encodeURIComponent(wsToken)}`
19 );
20 
21 ws.onopen = () => {
22 ws.send(JSON.stringify({
23 topic: websocketChannel,
24 event: "phx_join",
25 payload: {},
26 ref: "1",
27 }));
28 };
29 
30 ws.onmessage = (event) => {
31 const msg = JSON.parse(event.data);
32 
33 if (msg.event === "response") {
34 setResult(msg.payload.data);
35 setStatus("complete");
36 ws.close();
37 }
38 };
39 }
40 
41 return (
42 <div>
43 <button onClick={() => handleGenerate("Generate product description")}>
44 {status === "processing" ? "Processing..." : "Generate"}
45 </button>
46 {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
47 </div>
48 );
49}

Edge runtime

For Vercel Edge Functions, the webhook handler works the same way: just add the edge runtime export:

TYPESCRIPT
1// app/api/webhooks/modelriver/route.ts
2export const runtime = "edge";
3 
4// ... same handler code as above, using Web Crypto API instead of Node crypto

Note: On Edge, use crypto.subtle.importKey and crypto.subtle.sign instead of Node's crypto module for HMAC verification. Sign the same <timestamp>.<data JSON text> string, then hex-encode the result.


Best practices

  1. Respond quickly: Return 200 before processing heavy logic. Use Vercel's waitUntil for background work in serverless environments.
  2. Verify every webhook: Always validate the x-modelriver-signature header.
  3. Use server actions for triggering: Keep API keys server-side.
  4. Handle timeouts: Your callback must reach ModelRiver within 10 minutes.
  5. Log processing steps: Use structured logging for debugging backend pipeline flows.

Next steps