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-appcd my-ai-appEnvironment variables
Bash
# .envMODELRIVER_API_KEY=mr_live_YOUR_API_KEYMODELRIVER_WEBHOOK_SECRET=your_webhook_secretConfiguration
PHP
1// config/services.php2return [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.php2<?php3 4namespace App\Http\Middleware;5 6use Closure;7use Illuminate\Http\Request;8 9class VerifyModelRiverSignature10{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.php2<?php3 4namespace App\Http\Controllers;5 6use App\Jobs\ProcessAiWebhook;7use Illuminate\Http\Request;8use Illuminate\Http\JsonResponse;9 10class ModelRiverWebhookController extends Controller11{12 public function handle(Request $request): JsonResponse13 {14 $payload = $request->all();15 $type = $payload['type'] ?? '';16 $callbackUrl = $payload['callback_url'] ?? null;17 18 // Handle backend pipeline workflow19 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 webhook31 logger()->info('Standard webhook received', ['type' => $type]);32 return response()->json(['received' => true]);33 }34}Routes
PHP
1// routes/api.php2use App\Http\Controllers\ModelRiverWebhookController;3 4Route::post('/webhooks/modelriver', [ModelRiverWebhookController::class, 'handle'])5 ->middleware('verify.modelriver');Queued job
PHP
1// app/Jobs/ProcessAiWebhook.php2<?php3 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 ShouldQueue15{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(): void29 {30 try {31 $enrichedData = $this->aiResponse['data'] ?? [];32 33 // Your custom business logic34 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 service48 $enrichedData['reviewed'] = true;49 $enrichedData['reviewed_at'] = now()->toISOString();50 }51 52 // Call back to ModelRiver53 $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 callback71 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.php2<?php3 4namespace App\Services;5 6use Illuminate\Support\Facades\Http;7 8class ModelRiverService9{10 public function triggerAsync(string $workflow, string $prompt, array $metadata = []): array11 {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 controller28public 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
- Use queued jobs: Never block the webhook response with heavy processing.
- Set
$triesand$backoff: Handle transient failures with retries. - Use Laravel's HTTP client: Built-in retry, timeout, and error handling.
- Register middleware: Verify signatures before the controller is invoked.
- Use Laravel events: Emit events for extensibility (
WebhookReceived,CallbackSent).
Next steps
- Rails backend pipeline guide: Ruby alternative
- Webhooks reference: Retry policies and delivery monitoring
- Backend pipeline overview: Architecture and flow