Documentation

Backend pipeline with Laravel

Receive AI webhooks in Laravel controllers, process with queued jobs, and call back to ModelRiver: leveraging Laravel's queue and event system.

Overview

Laravel's powerful queue system, middleware pipeline, and HTTP client make it ideal for processing ModelRiver backend pipeline webhooks. Dispatch background jobs for heavy processing while responding instantly to the webhook.

What you'll build:

  • A webhook controller with signature verification middleware
  • Queued jobs for async processing and callback
  • HTTP client integration for ModelRiver callbacks
  • Event/listener pattern for extensibility

Quick start

Install dependencies

Bash
composer create-project laravel/laravel my-ai-app
cd my-ai-app

Environment variables

Bash
# .env
MODELRIVER_API_KEY=mr_live_YOUR_API_KEY
MODELRIVER_WEBHOOK_SECRET=your_webhook_secret

Configuration

PHP
1// config/services.php
2return [
3 // ...
4 'modelriver' => [
5 'api_key' => env('MODELRIVER_API_KEY'),
6 'webhook_secret' => env('MODELRIVER_WEBHOOK_SECRET'),
7 'base_url' => 'https://api.modelriver.com',
8 ],
9];

Signature verification middleware

PHP
1// app/Http/Middleware/VerifyModelRiverSignature.php
2<?php
3 
4namespace App\Http\Middleware;
5 
6use Closure;
7use Illuminate\Http\Request;
8 
9class VerifyModelRiverSignature
10{
11 public function handle(Request $request, Closure $next)
12 {
13 $signature = $request->header('mr-signature', '');
14 $secret = config('services.modelriver.webhook_secret');
15 
16 $expected = hash_hmac('sha256', $request->getContent(), $secret);
17 
18 if (!hash_equals($expected, $signature)) {
19 return response()->json(['error' => 'Invalid signature'], 401);
20 }
21 
22 return $next($request);
23 }
24}

Register the middleware:

PHP
1// bootstrap/app.php (Laravel 11+)
2->withMiddleware(function (Middleware $middleware) {
3 $middleware->alias([
4 'verify.modelriver' => \App\Http\Middleware\VerifyModelRiverSignature::class,
5 ]);
6})

Webhook controller

PHP
1// app/Http/Controllers/ModelRiverWebhookController.php
2<?php
3 
4namespace App\Http\Controllers;
5 
6use App\Jobs\ProcessAiWebhook;
7use Illuminate\Http\Request;
8use Illuminate\Http\JsonResponse;
9 
10class ModelRiverWebhookController extends Controller
11{
12 public function handle(Request $request): JsonResponse
13 {
14 $payload = $request->all();
15 $type = $payload['type'] ?? '';
16 $callbackUrl = $payload['callback_url'] ?? null;
17 
18 // Handle backend pipeline workflow
19 if ($type === 'task.ai_generated' && $callbackUrl) {
20 ProcessAiWebhook::dispatch(
21 event: $payload['event'] ?? '',
22 aiResponse: $payload['ai_response'] ?? [],
23 callbackUrl: $callbackUrl,
24 customerData: $payload['customer_data'] ?? [],
25 );
26 
27 return response()->json(['received' => true]);
28 }
29 
30 // Standard webhook
31 logger()->info('Standard webhook received', ['type' => $type]);
32 return response()->json(['received' => true]);
33 }
34}

Routes

PHP
1// routes/api.php
2use App\Http\Controllers\ModelRiverWebhookController;
3 
4Route::post('/webhooks/modelriver', [ModelRiverWebhookController::class, 'handle'])
5 ->middleware('verify.modelriver');

Queued job

PHP
1// app/Jobs/ProcessAiWebhook.php
2<?php
3 
4namespace App\Jobs;
5 
6use Illuminate\Bus\Queueable;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Bus\Dispatchable;
9use Illuminate\Queue\InteractsWithQueue;
10use Illuminate\Queue\SerializesModels;
11use Illuminate\Support\Facades\Http;
12use Illuminate\Support\Facades\Log;
13 
14class ProcessAiWebhook implements ShouldQueue
15{
16 use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
17 
18 public int $tries = 3;
19 public int $backoff = 10;
20 
21 public function __construct(
22 public string $event,
23 public array $aiResponse,
24 public string $callbackUrl,
25 public array $customerData,
26 ) {}
27 
28 public function handle(): void
29 {
30 try {
31 $enrichedData = $this->aiResponse['data'] ?? [];
32 
33 // Your custom business logic
34 if ($this->event === 'content_ready') {
35 $content = \App\Models\Content::create([
36 'title' => $enrichedData['title'] ?? '',
37 'body' => $enrichedData['description'] ?? '',
38 'category' => $this->customerData['category'] ?? 'general',
39 ]);
40 
41 $enrichedData['id'] = $content->id;
42 $enrichedData['slug'] = $content->slug;
43 $enrichedData['saved_at'] = now()->toISOString();
44 }
45 
46 if ($this->event === 'review_complete') {
47 // Call external service
48 $enrichedData['reviewed'] = true;
49 $enrichedData['reviewed_at'] = now()->toISOString();
50 }
51 
52 // Call back to ModelRiver
53 $response = Http::withHeaders([
54 'Authorization' => 'Bearer ' . config('services.modelriver.api_key'),
55 ])->timeout(10)->post($this->callbackUrl, [
56 'data' => $enrichedData,
57 'task_id' => "laravel_{$this->event}_" . now()->timestamp,
58 'metadata' => [
59 'processed_by' => 'laravel',
60 'processed_at' => now()->toISOString(),
61 ],
62 ]);
63 
64 $response->throw();
65 Log::info("✅ Callback sent for event: {$this->event}");
66 
67 } catch (\Exception $e) {
68 Log::error("❌ Callback failed: {$e->getMessage()}");
69 
70 // Send error callback
71 Http::withHeaders([
72 'Authorization' => 'Bearer ' . config('services.modelriver.api_key'),
73 ])->timeout(10)->post($this->callbackUrl, [
74 'error' => 'processing_failed',
75 'message' => $e->getMessage(),
76 ]);
77 
78 throw $e;
79 }
80 }
81}

Triggering async requests

PHP
1// app/Services/ModelRiverService.php
2<?php
3 
4namespace App\Services;
5 
6use Illuminate\Support\Facades\Http;
7 
8class ModelRiverService
9{
10 public function triggerAsync(string $workflow, string $prompt, array $metadata = []): array
11 {
12 $response = Http::withHeaders([
13 'Authorization' => 'Bearer ' . config('services.modelriver.api_key'),
14 ])->post(config('services.modelriver.base_url') . '/v1/ai/async', [
15 'model' => $workflow,
16 'messages' => [
17 ['role' => 'user', 'content' => $prompt],
18 ],
19 'metadata' => $metadata,
20 ]);
21 
22 $response->throw();
23 return $response->json();
24 }
25}
26 
27// Usage in a controller
28public function generate(Request $request, ModelRiverService $modelriver)
29{
30 $result = $modelriver->triggerAsync(
31 workflow: 'content_generator',
32 prompt: $request->input('prompt'),
33 metadata: ['user_id' => auth()->id()],
34 );
35 
36 return response()->json([
37 'channel_id' => $result['channel_id'],
38 'ws_token' => $result['ws_token'],
39 'websocket_channel' => $result['websocket_channel'],
40 ]);
41}

Best practices

  1. Use queued jobs: Never block the webhook response with heavy processing.
  2. Set $tries and $backoff: Handle transient failures with retries.
  3. Use Laravel's HTTP client: Built-in retry, timeout, and error handling.
  4. Register middleware: Verify signatures before the controller is invoked.
  5. Use Laravel events: Emit events for extensibility (WebhookReceived, CallbackSent).

Next steps