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-hereStep 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.



