You can integrate the OpenAI API (the API behind ChatGPT) with Laravel 13 using the built-in HTTP client. Store the API key in .env, wrap the call in a small service class, and send a POST request to the Responses endpoint from a controller on your server. The key must never reach the browser. This guide builds the whole flow, including validation, timeouts and a test that does not call OpenAI.

Targets: Laravel 13 (PHP 8.3 to 8.5) and the OpenAI Responses API. Model names and prices change often, so the model is a config value, and you should pick a current one from the OpenAI documentation.

Step 1: Get an API key

Create a key in your OpenAI platform account and add billing if the API requires it. Treat the key like a password. Put it in .env only:

OPENAI_API_KEY=your-secret-key OPENAI_MODEL=put-a-current-model-name-here 

Step 2: Register the config

Add an entry to config/services.php so the app reads settings through config(), which also works when the configuration is cached:

'openai' => [ 'key' => env('OPENAI_API_KEY'), 'model' => env('OPENAI_MODEL'), 'base_url' => env('OPENAI_BASE_URL', 'https://api.openai.com/v1'), ], 

Step 3: Write a service class

Keeping the API call in one class makes it easy to reuse and to fake in tests.

<?php namespace App\Services; use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; use RuntimeException; class OpenAiService { public function ask(string $prompt): string { try { $response = Http::withToken(config('services.openai.key')) ->baseUrl(config('services.openai.base_url')) ->timeout(60) ->retry(2, 500, throw: false) ->post('/responses', [ 'model' => config('services.openai.model'), 'input' => $prompt, ]) ->throw(); } catch (RequestException $e) { report($e); throw new RuntimeException('The AI service is not available right now.'); } return $this->extractText($response->json()); } private function extractText(array $payload): string { $text = ''; foreach ($payload['output'] ?? [] as $item) { foreach ($item['content'] ?? [] as $part) { if (($part['type'] ?? null) === 'output_text') { $text .= $part['text']; } } } return trim($text); } } 

The Responses API returns an output list. Each message item contains content parts, and the generated text is in parts whose type is output_text. The helper joins them. Check the current response shape in the OpenAI reference if you extend this code.

retry(2, 500, throw: false) retries failed requests twice with a 500 ms pause, and the following throw() raises an exception if the final answer is still an error.

Step 4: Create the controller and route

<?php namespace App\Http\Controllers; use App\Services\OpenAiService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use RuntimeException; class ChatController extends Controller { public function __invoke(Request $request, OpenAiService $ai): JsonResponse { $data = $request->validate([ 'message' => ['required', 'string', 'max:2000'], ]); try { $answer = $ai->ask($data['message']); } catch (RuntimeException $e) { return response()->json(['message' => $e->getMessage()], 503); } return response()->json(['answer' => $answer]); } } 
use App\Http\Controllers\ChatController; Route::post('/chat', ChatController::class)->middleware('throttle:20,1'); 

Limiting the message length and adding a throttle protect your bill. Each request costs money, so anonymous public endpoints should also require login.

Step 5: Try it

curl -X POST http://localhost:8000/api/chat \ -H "Accept: application/json" -H "Content-Type: application/json" \ -d '{"message":"Explain Laravel service providers in two sentences."}' 

Put the route in routes/api.php (run php artisan install:api first) or use a web route with CSRF protection for a form.

How do I test it without calling OpenAI?

Laravel can fake outgoing HTTP calls, so tests are fast and free:

use Illuminate\Support\Facades\Http; public function test_chat_returns_the_answer(): void { config(['services.openai.key' => 'test', 'services.openai.model' => 'test-model']); Http::fake([ '*/responses' => Http::response([ 'output' => [[ 'type' => 'message', 'content' => [['type' => 'output_text', 'text' => 'Hello from the fake']], ]], ]), ]); $this->postJson('/api/chat', ['message' => 'Hi']) ->assertOk() ->assertJson(['answer' => 'Hello from the fake']); } 

Common mistakes

  • Calling OpenAI from JavaScript: this exposes the key. Always go through your Laravel backend.
  • Hard-coding the model name: models are renamed and retired. Keep it in config.
  • No timeout: model calls can be slow. Set a timeout so PHP workers are not blocked forever.
  • Running long calls inside the web request at scale: for heavy use, dispatch a queued job and show the result later.
  • Logging prompts that contain private data: be careful what you store.

FAQ

Should I use the HTTP client or an OpenAI package?

The HTTP client has no extra dependency and is enough for simple calls. A package adds typed helpers if you need streaming or many endpoints.

Why do I get a 401 error?

The key is missing, wrong or revoked. Run php artisan config:clear after changing .env.

Why a 429 error?

You hit a rate or usage limit. Retry with a delay, reduce requests or check your plan's limits.

References

Originally published on yasiramin.dev.