n4nAI

Calling the OpenAI API from PHP with Guzzle

A hands-on tutorial for calling the OpenAI API from PHP using Guzzle. Build a client, stream responses, handle errors, and productionize your integration.

n4n Team2 min read529 words

Audio narration

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

Most PHP services still speak HTTP through Guzzle, not the official SDK. This tutorial builds a minimal, production-minded client for the php guzzle openai api workflow: authenticated requests, chat completions, streaming, and error handling without the overhead of a full library.

Prerequisites

You need a working PHP 8.1+ environment, Composer, and a valid OpenAI API key. The examples use Guzzle 7, which is the current major version and ships with Laravel’s HTTP client under the hood.

php -v
composer --version
export OPENAI_API_KEY="sk-..."
composer require guzzlehttp/guzzle

If you run Laravel, the illuminate/http client is already Guzzle-backed; the raw Guzzle code here translates directly.

Configure the Base Client

Instantiate a single Client with the OpenAI base URI and auth header. Reuse this instance across requests—Guzzle is safe for concurrent reuse and keeps connection pooling intact.

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.openai.com/v1/',
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('OPENAI_API_KEY'),
        'Content-Type' => 'application/json',
    ],
    'timeout' => 30,
]);

Set an explicit timeout. The OpenAI API can hang on slow generations; a missing timeout will tie up PHP workers.

Make Your First Chat Completion

The chat endpoint is a POST to chat/completions. Pass model, messages, and sampling params in the JSON body. Guzzle’s json option encodes the array and sets the content type automatically.

$response = $client->post('chat/completions', [
    'json' => [
        'model' => 'gpt-4o-mini',
        'messages' => [
            ['role' => 'system', 'content' => 'You are a concise assistant.'],
            ['role' => 'user', 'content' => 'What is the capital of France?'],
        ],
        'temperature' => 0.2,
    ],
]);

$body = json_decode($response->getBody(), true);
echo $body['choices'][0]['message']['content'];

Expected output:

Paris

That is the entire php guzzle openai api call for a non-streaming completion. No SDK required.

Understand the Response Shape

The decoded response looks like this (truncated):

{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "model": "gpt-4o-mini",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "Paris"},
      "finish_reason": "stop"
    }
  ],
  "usage": {"prompt_tokens": 14, "completion_tokens": 1, "total_tokens": 15}
}

Read usage if you meter cost. The finish_reason tells you whether the model stopped naturally or hit a token limit.

Stream Tokens Incrementally

For chat UIs, stream the response. Set stream: true in the API payload and stream: true in Guzzle’s request options. OpenAI returns Server-Sent Events (SSE) with data: {json} lines.

$response = $client->post('chat/completions', [
    'json' => [
        'model' => 'gpt-4o-mini',
        'messages' => [['role' => 'user', 'content' => 'Count to 5.']],
        'stream' => true,
    ],
    'stream' => true,
]);

$stream = $response->getBody();
$buffer = '';

while (!$stream->eof()) {
    $buffer .= $stream->read(256);
    while (($pos = strpos($buffer, "\n")) !== false) {
        $line = substr($buffer, 0, $pos);
        $buffer = substr($buffer, $pos + 1);

        if (!str_starts_with($line, 'data: ')) {
            continue;
        }
        $data = substr($line, 6);
        if ($data === '[DONE]') {
            break 2;
        }
        $chunk = json_decode($data, true);
        echo $chunk['choices'][0]['delta']['content'] ?? '';
        flush();
    }
}

Expected output (printed token by token):

1
2
3
4
5

The delta.content field carries incremental text. Ignore chunks where it is absent—they often contain role or finish metadata.

Handle Errors and Rate Limits

OpenAI returns 429 on rate limits and 5xx on upstream faults. Guzzle throws RequestException for any non-2xx status unless you disable http_errors. Catch it and inspect the response.

use GuzzleHttp\Exception\RequestException;

try {
    $response = $client->post('chat/completions', [/* ... */]);
} catch (RequestException $e) {
    if ($e->hasResponse()) {
        $status = $e->getResponse()->getStatusCode();
        $detail = json_decode($e->getResponse()->getBody(), true);
        error_log("OpenAI error {$status}: " . ($detail['error']['message'] ?? 'unknown'));
    }
    // retry or fail
}

A simple exponential backoff for 429 keeps you compliant with the API:

$attempt = 0;
while ($attempt < 3) {
    try {
        return $client->post('chat/completions', [/* ... */]);
    } catch (RequestException $e) {
        if ($e->getResponse()?->getStatusCode() === 429) {
            sleep(2 ** $attempt);
            $attempt++;
            continue;
        }
        throw $e;
    }
}

Production Considerations

Never hardcode the API key. Pull it from environment or a secret manager, and rotate it if committed. Set Guzzle’s connect_timeout separately from timeout so DNS or TLS stalls fail fast.

If you need multi-provider resilience, an OpenAI-compatible gateway like n4n.ai provides automatic fallback when a provider is rate-limited or degraded, and the Guzzle request shape above works unchanged—same /chat/completions path, same auth header.

For Laravel, wrap the client in a service class and bind it to the container. The framework’s HTTP client is fine for simple calls, but raw Guzzle gives you finer control over streaming and middleware.

Extending to Function Calls

The same php guzzle openai api pattern supports tool calls. Add a tools array to the JSON body and parse choices[0].message.tool_calls in the response. Streaming works identically; the delta will contain partial tool_calls arguments that you must concatenate before JSON-decoding.

'json' => [
    'model' => 'gpt-4o-mini',
    'messages' => $messages,
    'tools' => [[
        'type' => 'function',
        'function' => [
            'name' => 'get_weather',
            'parameters' => ['type' => 'object', 'properties' => ['city' => ['type' => 'string']]],
        ],
    ]],
],

Treat the non-streaming path as the source of truth for final arguments; stream only for UX.

Closing Notes on Latency

Guzzle blocks on read() during streaming, which is acceptable for CLI or worker scripts. In a PHP-FPM web request, flush output only if your frontend reads chunks; otherwise buffer and return the full string. For high concurrency, consider ReactPHP or Laravel Octane with async Guzzle handlers—but for most backend integrations, the synchronous client is simpler and fast enough.

Tagsphpguzzleopenai-apitutorial

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 →