How to Integrate OpenAI API with Laravel (Complete Guide 2026)

Laravel is the most popular PHP framework, and adding OpenAI to it is straightforward with the official openai-php/client library. This guide walks through a complete integration: installing the SDK, creating a service class, building a chat controller, streaming responses, and handling errors. By the end you’ll have a working AI chat feature in your Laravel app.

For a lower-level look at the PHP SDK itself, see OpenAI PHP SDK Guide. For using OpenAI without Laravel, see OpenAI PHP Tutorial.


Requirements

  • Laravel 10 or 11
  • PHP 8.1+
  • Composer
  • OpenAI API key

Step 1: Install the OpenAI PHP Client

Install the official OpenAI PHP SDK via Composer:

composer require openai-php/laravel

Then publish the config file:

php artisan vendor:publish --provider="OpenAI\\Laravel\\ServiceProvider"

This creates config/openai.php which reads the API key from your environment.


Step 2: Configure the API Key

Add your OpenAI API key to .env:

OPENAI_API_KEY=sk-proj-...
OPENAI_ORGANIZATION=  # optional

Step 3: Create the OpenAI Service

Create app/Services/OpenAIService.php:

<?php

namespace App\Services;

use OpenAI\Laravel\Facades\OpenAI;

class OpenAIService
{
    public function chat(array $messages, string $model = 'gpt-4o-mini'): string
    {
        $response = OpenAI::chat()->create([
            'model' => $model,
            'messages' => $messages,
            'max_tokens' => 1024,
            'temperature' => 0.7,
        ]);

        return $response->choices[0]->message->content;
    }

    public function stream(array $messages, string $model = 'gpt-4o-mini'): \Generator
    {
        $stream = OpenAI::chat()->createStreamed([
            'model' => $model,
            'messages' => $messages,
        ]);

        foreach ($stream as $response) {
            $delta = $response->choices[0]->delta->content;
            if ($delta !== null) {
                yield $delta;
            }
        }
    }
}

Step 4: Create the Chat Controller

php artisan make:controller ChatController

Edit app/Http/Controllers/ChatController.php:

<?php

namespace App\Http\Controllers;

use App\Services\OpenAIService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class ChatController extends Controller
{
    public function __construct(private OpenAIService $openAI) {}

    public function index()
    {
        return view('chat.index');
    }

    public function send(Request $request): JsonResponse
    {
        $request->validate([
            'message' => 'required|string|max:2000',
            'history' => 'array',
        ]);

        $messages = $request->input('history', []);
        $messages[] = ['role' => 'user', 'content' => $request->input('message')];

        $reply = $this->openAI->chat($messages);

        return response()->json([
            'reply' => $reply,
            'messages' => array_merge($messages, [
                ['role' => 'assistant', 'content' => $reply],
            ]),
        ]);
    }

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

        $messages = [
            ['role' => 'system', 'content' => 'You are a helpful assistant.'],
            ['role' => 'user', 'content' => $request->input('message')],
        ];

        return response()->stream(function () use ($messages) {
            foreach ($this->openAI->stream($messages) as $chunk) {
                echo "data: " . json_encode(['chunk' => $chunk]) . "\n\n";
                ob_flush();
                flush();
            }
            echo "data: [DONE]\n\n";
        }, 200, [
            'Content-Type' => 'text/event-stream',
            'Cache-Control' => 'no-cache',
            'X-Accel-Buffering' => 'no',
        ]);
    }
}

Step 5: Register Routes

Add to routes/web.php:

use App\Http\Controllers\ChatController;

Route::get('/chat', [ChatController::class, 'index'])->name('chat.index');
Route::post('/chat/send', [ChatController::class, 'send'])->name('chat.send');
Route::post('/chat/stream', [ChatController::class, 'stream'])->name('chat.stream');

Step 6: Create the Blade View

Create resources/views/chat/index.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>AI Chat</title>
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
        #messages { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 16px;
                    border-radius: 8px; margin-bottom: 16px; background: #f9f9f9; }
        .user  { color: #1a56db; font-weight: 600; margin-bottom: 8px; }
        .assistant { color: #111; margin-bottom: 8px; }
        #form  { display: flex; gap: 8px; }
        #input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; }
        button { padding: 10px 20px; background: #1a56db; color: #fff; border: none;
                 border-radius: 6px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>AI Chat</h1>
    <div id="messages"></div>
    <form id="form">
        <input id="input" type="text" placeholder="Ask something..." autocomplete="off">
        <button type="submit">Send</button>
    </form>
    <script>
    const msgs = document.getElementById('messages');
    const input = document.getElementById('input');
    let history = [];

    function add(role, text) {
        const d = document.createElement('div');
        d.className = role;
        d.innerHTML = '<strong>' + (role === 'user' ? 'You' : 'AI') + ':</strong> ' + text;
        msgs.appendChild(d);
        msgs.scrollTop = msgs.scrollHeight;
    }

    document.getElementById('form').addEventListener('submit', async (e) => {
        e.preventDefault();
        const text = input.value.trim();
        if (!text) return;
        input.value = '';
        add('user', text);

        const res = await fetch('/chat/send', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
            },
            body: JSON.stringify({ message: text, history }),
        });
        const data = await res.json();
        add('assistant', data.reply);
        history = data.messages;
    });
    </script>
</body>
</html>

Error Handling

Wrap OpenAI calls to handle API errors and network failures:

use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\TransporterException;

try {
    $reply = $this->openAI->chat($messages);
} catch (ErrorException $e) {
    // API error: invalid key, quota exceeded, invalid request
    Log::error('OpenAI API error', ['message' => $e->getMessage()]);
    return response()->json(['error' => 'AI service error. Please try again.'], 503);
} catch (TransporterException $e) {
    // Network timeout or connection error
    Log::error('OpenAI transport error', ['message' => $e->getMessage()]);
    return response()->json(['error' => 'Connection failed. Please try again.'], 503);
}

Caching Responses

Cache identical prompts to reduce API costs:

use Illuminate\Support\Facades\Cache;

public function chatCached(string $userMessage): string
{
    $key = 'openai_' . md5($userMessage);

    return Cache::remember($key, 3600, function () use ($userMessage) {
        return $this->chat([['role' => 'user', 'content' => $userMessage]]);
    });
}

Testing with Fakes

The package ships a OpenAI::fake() helper so you never hit the real API in tests:

use OpenAI\Laravel\Facades\OpenAI;
use OpenAI\Responses\Chat\CreateResponse;

public function test_chat_returns_reply(): void
{
    OpenAI::fake([
        CreateResponse::fake([
            'choices' => [
                ['message' => ['content' => 'Hello from AI!']],
            ],
        ]),
    ]);

    $service = app(OpenAIService::class);
    $reply = $service->chat([['role' => 'user', 'content' => 'Hi']]);

    $this->assertEquals('Hello from AI!', $reply);
}

Summary

  • Install via composer require openai-php/laravel and set OPENAI_API_KEY in .env
  • Wrap API calls in a service class — keeps controllers clean and testable
  • Use response()->stream() with SSE for real-time streaming output
  • Handle ErrorException and TransporterException for robust error handling
  • Use OpenAI::fake() for unit testing without real API calls

For more PHP + AI content, see How to Use OpenAI API with PHP and How to Build an AI Chatbot with PHP.


Subscribe to my newsletter — practical guides on Claude API, AI agents, RAG, and automation.

Subscribe