n4nAI

Building a Laravel chatbot backend with n4n

Step-by-step tutorial to build a Laravel chatbot backend with n4n, using OpenAI-compatible endpoints, session history, and streaming.

n4n Team3 min read570 words

Audio narration

Coming soon — every post will get a voice note here.

Building a laravel chatbot backend n4n is straightforward if you treat the gateway as a drop-in OpenAI-compatible service. This tutorial walks through a production-shaped implementation in Laravel 11, covering session-based conversation state, non-streaming and streaming responses, and the few gotchas that bite when you move past a hello-world script.

Prerequisites

  • PHP 8.2 or newer
  • Laravel 11 installed (either a fresh project or an existing app)
  • Composer for dependency management
  • An API key from n4n.ai (we’ll use its OpenAI-compatible endpoint at https://api.n4n.ai/v1)
  • Basic comfort with Laravel routes, controllers, and Blade templates

If you are adding this to an existing Laravel app, skip the project creation step. The code below assumes you can run php artisan commands and edit routes/web.php.

Project setup

Create a fresh project if needed:

composer create-project laravel/laravel chatbot-backend
cd chatbot-backend

Generate the controller that will handle chat logic:

php artisan make:controller ChatController

We will place chat routes in routes/web.php rather than routes/api.php. This is deliberate: Laravel’s api middleware group is stateless and does not start a session. Our demo stores conversation history in the session, so the web group is required. If you later persist history to a database, you can move to the API group.

Configuring the gateway

Store credentials in .env:

N4N_API_KEY=sk-your-key-here
N4N_BASE_URL=https://api.n4n.ai/v1

Register a service config in config/services.php so the laravel chatbot backend n4n stays configurable:

'n4n' => [
    'key' => env('N4N_API_KEY'),
    'base_url' => env('N4N_BASE_URL', 'https://api.n4n.ai/v1'),
],

Never hardcode keys in controllers. Pulling from config also makes testing with mocked HTTP easier.

Building the chat controller

Open app/Http/Controllers/ChatController.php. We will implement conversation state, a standard request, and a streaming request.

Handling conversation state

Session storage keeps the message array in the user’s cookie-backed session:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Session;

class ChatController extends Controller
{
    private function history(): array
    {
        return Session::get('chat_history', []);
    }

    private function addMessage(string $role, string $content): void
    {
        $history = $this->history();
        $history[] = ['role' => $role, 'content' => $content];
        Session::put('chat_history', $history);
    }
}

In a real deployment you would scope history per authenticated user and persist to a conversations table. For this tutorial, session is enough to demonstrate the flow.

Calling the model (non-streaming)

Add the send method:

    public function send(Request $request)
    {
        $request->validate(['message' => 'required|string|max:4000']);

        $this->addMessage('user', $request->input('message'));

        $response = Http::withToken(config('services.n4n.key'))
            ->post(config('services.n4n.base_url') . '/chat/completions', [
                'model' => 'openai/gpt-4o-mini',
                'messages' => $this->history(),
                'temperature' => 0.7,
            ]);

        if ($response->failed()) {
            return response()->json(['error' => 'Upstream call failed'], 502);
        }

        $data = $response->json();
        $reply = $data['choices'][0]['message']['content'] ?? null;

        if (!$reply) {
            return response()->json(['error' => 'No completion'], 502);
        }

        $this->addMessage('assistant', $reply);

        return response()->json(['reply' => $reply, 'history' => $this->history()]);
    }

The request shape matches the OpenAI Chat Completions API exactly. Because the laravel chatbot backend n4n sits behind an OpenAI-compatible endpoint, you can change 'model' to any of the 240+ addressed models without altering client code.

Expected output when you POST {"message":"What is Laravel?"}:

{
  "reply": "Laravel is a PHP web application framework with expressive, elegant syntax...",
  "history": [
    {"role": "user", "content": "What is Laravel?"},
    {"role": "assistant", "content": "Laravel is a PHP web application framework with expressive, elegant syntax..."}
  ]
}

Adding a minimal frontend

To interact without curl, add routes and a Blade view.

routes/web.php:

Route::get('/chat', fn () => view('chat'));
Route::post('/chat/send', [ChatController::class, 'send']);
Route::post('/chat/stream', [ChatController::class, 'stream']);

resources/views/chat.blade.php:

<!DOCTYPE html>
<html>
<body>
<textarea id="msg" placeholder="Type..."></textarea>
<button onclick="send()">Send</button>
<pre id="out"></pre>
<script>
async function send() {
    const res = await fetch('/chat/send', {
        method: 'POST',
        headers: {'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}'},
        body: JSON.stringify({message: document.getElementById('msg').value})
    });
    const data = await res.json();
    document.getElementById('out').textContent = JSON.stringify(data, null, 2);
}
</script>
</body>
</html>

The CSRF token is required because the web middleware group enforces it. If you test with curl, disable CSRF for the route or use the api token approach.

Testing the endpoint

Run the dev server:

php artisan serve

In a separate shell:

curl -X POST http://localhost:8000/chat/send \
  -H "Content-Type: application/json" \
  -d '{"message":"Explain PHP generators"}'

You should receive JSON containing the model’s reply and the updated history. A 502 indicates an upstream failure—verify the API key and base URL.

Streaming responses

A chatbot feels responsive when tokens arrive incrementally. n4n.ai honors the stream: true parameter, returning Server-Sent Events. Extend the controller:

    public function stream(Request $request)
    {
        $request->validate(['message' => 'required|string|max:4000']);
        $this->addMessage('user', $request->input('message'));

        $payload = [
            'model' => 'openai/gpt-4o-mini',
            'messages' => $this->history(),
            'stream' => true,
            'temperature' => 0.7,
        ];

        $response = Http::withToken(config('services.n4n.key'))
            ->withOptions(['stream' => true])
            ->post(config('services.n4n.base_url') . '/chat/completions', $payload);

        return response()->stream(function () use ($response) {
            $buffer = '';
            foreach ($response->toPsrResponse()->getBody() as $chunk) {
                $buffer .= $chunk;
                while (($pos = strpos($buffer, "\n\n")) !== false) {
                    $line = substr($buffer, 0, $pos);
                    $buffer = substr($buffer, $pos + 2);
                    if (str_starts_with($line, 'data: ')) {
                        $data = substr($line, 6);
                        if ($data === '[DONE]') {
                            echo "data: [DONE]\n\n";
                            ob_flush();
                            flush();
                            return;
                        }
                        $json = json_decode($data, true);
                        $token = $json['choices'][0]['delta']['content'] ?? '';
                        if ($token) {
                            echo "data: " . json_encode(['token' => $token]) . "\n\n";
                            ob_flush();
                            flush();
                        }
                    }
                }
            }
        }, 200, ['Content-Type' => 'text/event-stream']);
    }

This parser splits the SSE byte stream on blank lines and forwards token deltas. In production, use a dedicated SSE client or Laravel’s broadcasting system, but the snippet proves the integration.

Production considerations

The laravel chatbot backend n4n should survive provider hiccups. n4n.ai provides automatic fallback when a provider is rate-limited or degraded, so you avoid writing custom provider-pinning retry loops for that case. Still, add a basic retry for transient network errors:

Http::withToken(config('services.n4n.key'))
    ->retry(3, 100)
    ->post(...)

Per-token usage metering arrives in the usage object of every completion response. Capture it for cost attribution:

$usage = $data['usage'] ?? null;
// $usage['prompt_tokens'], $usage['completion_tokens'], $usage['total_tokens']

If you send cache_control metadata in messages (where the underlying model supports it), the gateway forwards those hints, letting you trim repeat-prompt costs.

Bound the history length to prevent context overflow. Before calling the model, slice the array:

$history = array_slice($this->history(), -20);

That yields a robust, runnable core. From here, layer on authentication, rate limiting via ThrottleRequests, and database-backed conversations.

Tagslaraveln4nchatbotbackend

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All php & laravel llm integration posts →