OpenAI PHP Client
A community-maintained PHP client for the OpenAI REST API, with streaming support. Requires PHP 7.4+ and the native cURL and JSON extensions, with no third-party runtime dependencies.
See API_COVERAGE.md for endpoint coverage and MIGRATING.md for breaking changes from earlier versions.
Installation
composer require orhanerday/open-ai
Quick start
Set OPENAI_API_KEY in your environment.
Linux or macOS:
export OPENAI_API_KEY='your-api-key'
PowerShell:
$Env:OPENAI_API_KEY = "your-api-key"
Windows Command Prompt:
set "OPENAI_API_KEY=your-api-key"
Save this as example.php:
<?php
require __DIR__ . '/vendor/autoload.php';
use Orhanerday\OpenAi\OpenAi;
$open_ai = new OpenAi(getenv('OPENAI_API_KEY'));
$result = $open_ai->chat([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'Hello']],
]);
$response = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
echo $response['choices'][0]['message']['content'];
Run it with php example.php. See Handling results for HTTP errors.
Any other endpoint
The SDK has dedicated methods for the most common APIs (see Supported APIs).
Use request() for REST endpoints without dedicated methods, such as Containers, Evals,
organization administration, and Vaults. It supports JSON and multipart requests under the
configured API prefix. WebSocket connections and workload identity token exchange need a separate client.
public function request(
string $method, // GET, POST, PUT, PATCH or DELETE (case-insensitive)
string $path, // relative path starting with "/", e.g. "/containers/cntr_123"
?array $opts = null, // query parameters (GET) or request body (other methods)
?callable $stream = null,// required when the request streams
bool $multipart = false // force multipart/form-data
);
How it works
request() runs through the same code as every built-in method. That means:
- The full URL is
base URL+/v1+path, sorequest('GET', '/models')callshttps://api.openai.com/v1/models. - Your API key,
setORG(),setHeader(),setProxy(),setTimeout(), andsetCURLOptions()apply. setBaseURL()andsetApiVersion()apply, so it also works with OpenAI-compatible providers.- Ordinary calls return the raw response body as a string. Streaming calls deliver chunks to
the callback; decode JSON responses with
json_decode().
$method |
What $opts becomes |
|---|---|
GET |
Query string. Nested arrays are encoded as filter[key]=value, lists as include[]=a&include[]=b, booleans as true/false. |
POST, PUT, PATCH |
JSON body. An empty array sends {}. |
DELETE |
No body unless you pass $opts. |
Non-GET, with $multipart = true or a file/image key |
multipart/form-data body. |
Examples
Create, list, and delete a resource:
// POST with a JSON body
$created = json_decode($open_ai->request('POST', '/containers', ['name' => 'my-container']), true);
// GET with query parameters: /v1/containers?limit=10&order=desc
$list = json_decode($open_ai->request('GET', '/containers', ['limit' => 10, 'order' => 'desc']), true);
// DELETE a resource by ID
$open_ai->request('DELETE', '/containers/' . $created['id']);
Evals:
$evals = json_decode($open_ai->request('GET', '/evals', ['limit' => 5]), true);
Organization administration (requires an admin key; replace the key with an admin key for these calls):
$admin = new OpenAi(getenv('OPENAI_ADMIN_KEY'));
$projects = json_decode($admin->request('GET', '/organization/projects', ['limit' => 20]), true);
Query parameters with arrays and booleans:
// /v1/responses/resp_123?include[]=message.output_text.logprobs&stream=false
$open_ai->request('GET', '/responses/resp_123', [
'include' => ['message.output_text.logprobs'],
'stream' => false,
]);
Streaming. Pass a callback as the fourth argument. It receives raw server-sent event chunks and must return the number of bytes it handled:
$open_ai->request('POST', '/responses', [
'model' => 'gpt-4o-mini',
'input' => 'Write a haiku about PHP.',
'stream' => true,
], function ($curl, $chunk) {
echo $chunk;
return strlen($chunk);
});
File upload. A file key switches to multipart automatically. Pass true as the fifth argument when your body needs multipart without a file or image key:
$open_ai->request('POST', '/files', [
'purpose' => 'user_data',
'file' => new CURLFile('/path/to/data.jsonl'),
]);
Error handling
request() throws an Exception for an unsupported method, an invalid path, a missing API key,
or a cURL failure such as a timeout. By default, HTTP errors from the API (4xx and 5xx) return
their response bodies; check the status code and the error field, or enable
setThrowOnError(true):
try {
$body = json_decode($open_ai->request('GET', '/containers/unknown'), true);
$status = $open_ai->getCURLInfo()['http_code'];
if ($status >= 400) {
echo "API error $status: " . ($body['error']['message'] ?? 'unknown error');
}
} catch (Exception $e) {
echo 'Request failed: ' . $e->getMessage();
}
Security notes
- Paths must start with a single
/. Absolute URLs,//host/...paths, and any path containing://are rejected. To target another host, callsetBaseURL()once. - Do not pass untrusted user input directly as
$path. Validate or encode IDs first, for example withrawurlencode().
Finding the right route
Look up the path, method, parameters, and any required beta headers in the API reference. Dedicated methods and transport limitations are listed in API_COVERAGE.md.
Supported APIs
- Chat
- [x] Chat completions
- [x] Stored completions: retrieve, update, delete, list, and list messages
- Models
- [x] List models
- [x] Retrieve model
- Images
- [x] Create image
- [x] Create image edit
- Embeddings
- Audio
- [x] Text to Speech (TTS)
- [x] Create transcription
- [x] Create translation
- [x] Custom voice creation and voice consent CRUD
- Files
- [x] List files
- [x] Upload file
- [x] Delete file
- [x] Retrieve file
- [x] Retrieve file content
- Fine-tunes
- [x] Create fine-tune
- [x] List fine-tunes
- [x] Retrieve fine-tune
- [x] Cancel fine-tune
- [x] List fine-tune events
- [x] Delete fine-tune model
- [x] Pause/resume, checkpoints and permissions, grader validation and execution
- Moderation
- Responses API
- [x] Responses
- Conversations API
- [x] Conversations
- Agents API (beta)
- Vector Stores API
- [x] Vector Stores
- Batches API
- [x] Batches
- Uploads API
- [x] Uploads
- Realtime API
- Decisions API
- [x] Decisions
- Webhooks
- Organization reporting
- [x] Usage and Costs
- Any other endpoint
- [x] Generic
request()for compatible REST operations in Containers, Evals, Administration, Vaults, and other families
- [x] Generic
See API_COVERAGE.md for exact routes, guide support, and testing limits. WebSocket mode and mid-turn steering require a separate WebSocket client.
Configuration
The examples below use an OpenAi instance:
use Orhanerday\OpenAi\OpenAi;
$open_ai = new OpenAi(getenv('OPENAI_API_KEY'));
You can also configure or replace the key after construction:
$open_ai = new OpenAi();
$open_ai->setApiKey(getenv('OPENAI_API_KEY'));
setApiKey() replaces the Authorization header and rejects blank keys. Requests fail locally
if no key is configured.
Organization
Specify the organization for API requests:
$open_ai->setORG('org-your-organization');
Base URL
Set the origin for an OpenAI-compatible API:
$open_ai->setBaseURL('https://ai.example.com');
Custom API Version
By default, the client appends /v1 to the Base URL. You can override this to support alternative API versions (e.g., ByteDance Ark uses /api/v3):
$open_ai->setBaseURL('https://ark.cn-beijing.volces.com');
$open_ai->setApiVersion('api/v3');
Proxy
Route requests through a proxy:
$open_ai->setProxy("http://127.0.0.1:1086");
Custom cURL options
Set cURL options for each request:
$open_ai->setCURLOptions([
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 300,
]);
Headers
$open_ai->setHeader(["Connection: keep-alive"]);
Request information
Inspect transport metadata after a request:
$open_ai->listModels();
$info = $open_ai->getCURLInfo();
echo $info['http_code'];
Chat completions
$result = $open_ai->chat([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'Hello']],
]);
Read response text
$response = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
echo $response['choices'][0]['message']['content'];
Stream example
Both chat() and createResponse() accept a callback when stream is true.
The callback receives raw server-sent event chunks and must return the number of bytes consumed.
Forward chunks unchanged so event boundaries survive browser parsing.
$open_ai = new OpenAi(getenv('OPENAI_API_KEY'));
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$open_ai->createResponse([
'model' => 'gpt-4o-mini',
'input' => 'Hello',
'stream' => true,
'store' => false,
], function ($curl, $data) {
echo $data;
if (ob_get_level() > 0) {
ob_flush();
}
flush();
return strlen($data);
});
For a browser client, listen for the named Responses events:
See the official streaming guide for event handling.
Images
Use a supported GPT Image model explicitly. GPT Image responses contain base64 image data. See the official image guide for supported models and parameters.
Create image
$result = $open_ai->image([
'model' => 'gpt-image-2.5-flare',
'prompt' => 'A cat drinking milk',
'n' => 1,
'size' => '1024x1024',
]);
$image = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
file_put_contents('cat.png', base64_decode($image['data'][0]['b64_json']));
Create image edit
Supply your own input image. A single local file is sent as multipart form data.
For multiple inputs, use the JSON images field with uploaded file IDs or image URLs.
$result = $open_ai->imageEdit([
'model' => 'gpt-image-2.5-flare',
'image' => new CURLFile('/path/to/input.png', 'image/png', 'input.png'),
'prompt' => 'Add a beret to the animal in this image',
'n' => 1,
'size' => '1024x1024',
]);
To create a variation, use imageEdit() with a prompt describing the desired changes.
Embeddings
Create a vector representation of text.
Related guide: Embeddings
Create embeddings
$result = $open_ai->embeddings([
"model" => "text-embedding-3-small",
"input" => "The food was delicious and the waiter..."
]);
Content Moderations
Classify content using the moderation endpoint.
$flags = $open_ai->moderation([
'model' => 'omni-moderation-latest',
'input' => 'I hate ducks!'
]);
API reference: OpenAI Moderations
Audio
Text to speech (TTS)
tts($opts) wraps /v1/audio/speech for existing integrations. Its currently documented speech models are deprecated;
see OpenAI's deprecations and the
Realtime guide when planning a new voice integration.
Pass a model supported by the speech endpoint explicitly; Realtime models use a different API.
Create transcription
Transcribe a recording using a file you supply:
$result = $open_ai->transcribe([
'model' => 'gpt-transcribe',
'file' => new CURLFile('/path/to/recording.mp3', 'audio/mpeg', 'recording.mp3'),
]);
See the official file transcription guide.
Create translation
translate($opts) wraps /v1/audio/translations for existing integrations. For new workflows, transcribe with
gpt-transcribe, then send the transcript to createResponse() with translation instructions.
Check the translation endpoint's supported models
before using the compatibility wrapper.
Files
Upload files for model input, vector store file search, batch processing, or fine-tuning. Choose a purpose accepted by the Files API.
List files
Returns a list of files that belong to the user's organization.
$files = $open_ai->listFiles();
Upload file
Upload a file you supply, and retain the returned ID for later operations. For larger files, use the Uploads API. See the Files reference for current size limits.
$result = $open_ai->uploadFile([
'purpose' => 'user_data',
'file' => new CURLFile('/path/to/document.txt', 'text/plain', 'document.txt'),
]);
$uploaded = json_decode($result, true, 512, JSON_THROW_ON_ERROR);
$fileId = $uploaded['id'];
Upload file with an HTML form
Select file to upload:
<?php
require __DIR__ . '/vendor/autoload.php';
use Orhanerday\OpenAi\OpenAi;
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
ob_clean();
$open_ai = new OpenAi(getenv('OPENAI_API_KEY'));
$tmp_file = $_FILES['fileToUpload']['tmp_name'];
$file_name = basename($_FILES['fileToUpload']['name']);
$c_file = curl_file_create($tmp_file, $_FILES['fileToUpload']['type'], $file_name);
echo "[";
echo $open_ai->uploadFile(
[
"purpose" => "user_data",
"file" => $c_file,
]
);
echo ",";
echo $open_ai->listFiles();
echo "]";
}
Delete file
$result = $open_ai->deleteFile($fileId);
Retrieve file
$file = $open_ai->retrieveFile($fileId);
Retrieve file content
Use an ID for a downloadable file purpose, such as a fine-tuning or batch file. OpenAI rejects
content downloads for user_data files, even though retrieving their metadata succeeds.
$file = $open_ai->retrieveFileContent($downloadableFileId);
Fine-tunes
These methods manage /v1/fine_tuning/jobs. Fine-tuning access is restricted by OpenAI's
availability policy.
Use a model supported for fine-tuning and a training file containing valid chat-format examples.
Create fine-tune
$upload = $open_ai->uploadFile([
'purpose' => 'fine-tune',
'file' => new CURLFile('/path/to/training.jsonl', 'application/jsonl', 'training.jsonl'),
]);
$trainingFileId = json_decode($upload, true, 512, JSON_THROW_ON_ERROR)['id'];
$result = $open_ai->createFineTune([
'model' => 'gpt-4o-mini',
'training_file' => $trainingFileId,
]);
$jobId = json_decode($result, true, 512, JSON_THROW_ON_ERROR)['id'];
List fine-tune
$jobs = $open_ai->listFineTunes();
Retrieve fine-tune
$job = $open_ai->retrieveFineTune($jobId);
Cancel fine-tune
$result = $open_ai->cancelFineTune($jobId);
List fine-tune events
$events = $open_ai->listFineTuneEvents($jobId);
Delete fine-tune model
Delete the model ID returned by a completed job, using DELETE /v1/models/{model}:
$completedJob = json_decode($open_ai->retrieveFineTune($jobId), true, 512, JSON_THROW_ON_ERROR);
if (!empty($completedJob['fine_tuned_model'])) {
$result = $open_ai->deleteFineTune($completedJob['fine_tuned_model']);
}
Checkpoint permissions
Construct a separate OpenAi client with an admin API key for checkpoint permissions.
listFineTuningCheckpointPermissions(), createFineTuningCheckpointPermission(), and
deleteFineTuningCheckpointPermission() manage access across projects;
listFineTuningCheckpoints() lists a job's checkpoints.
Models
List or retrieve model metadata.
List models
$result = $open_ai->listModels();
Retrieve model
$result = $open_ai->retrieveModel("gpt-4o-mini");
Handling results
By default, nonstreaming methods return the raw response body as a string, and HTTP API errors (4xx/5xx) do not throw exceptions. JSON, downloaded files, and audio keep their original format.
1. Structured Response Object
Inspect the most recent response via getLastResponse():
$raw = $open_ai->chat([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'Hello']],
]);
$response = $open_ai->getLastResponse();
if ($response->ok()) {
$data = $response->json(); // Decoded JSON array
echo $data['choices'][0]['message']['content'];
} else {
echo "HTTP Status: " . $response->status() . "\n";
echo "Error Body: " . $response->body() . "\n";
}
// Access headers (case-insensitive)
echo $response->header('openai-processing-ms');
The Response object provides:
$response->status(): HTTP status code integer$response->ok():truefor 2xx status codes$response->json(bool $assoc = true): Decodes JSON directly$response->body(): Raw response body string$response->headers(): Headers from the final HTTP response, keyed by lowercase names$response->header(string $name): Case-insensitive header lookup
Header collection also works when you configure your own CURLOPT_HEADERFUNCTION; the callback
still runs and must return the number of bytes consumed. Successful streaming responses deliver
their body to the callback, so Response::body() is empty. HTTP error bodies remain available
in Response and OpenAiException.
getLastResponse() returns null before the first completed request. A transport failure leaves
the previous response available; catch the exception before treating that response as the current result.
json() throws JsonException for an empty body, binary download, or other invalid JSON.
2. Automatic Exception Handling (setThrowOnError)
If you prefer exceptions over manual status code checking, enable setThrowOnError(true). When enabled, any HTTP 4xx or 5xx status throws an OpenAiException:
use Orhanerday\OpenAi\OpenAiException;
$open_ai->setThrowOnError(true);
try {
$raw = $open_ai->chat([
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'Hello']],
]);
} catch (OpenAiException $e) {
echo "HTTP Status: " . $e->getStatusCode() . "\n";
echo "Error Type: " . $e->getErrorType() . "\n"; // e.g. 'invalid_request_error'
echo "Error Code: " . $e->getErrorCode() . "\n"; // e.g. 'rate_limit_exceeded'
echo "Message: " . $e->getMessage() . "\n";
print_r($e->getApiError()); // Decoded error object
} catch (Exception $e) {
// Network / cURL failures (e.g. timeout, connection dropped)
echo "Connection error: " . $e->getMessage();
}
3. Automatic Retries & Rate-Limit Backoff
Enable automatic retries for rate limits (HTTP 429) and transient server errors (HTTP 500, 502, 503, 504 on idempotent requests):
// Retry up to 3 times, starting with a 500ms exponential backoff
$open_ai->setRetries(3, 500);
maxRetriescounts extra attempts after the initial request;0disables retries.- Uses exponential backoff with small random jitter, capped at 60 seconds. Numeric and HTTP-date
Retry-Aftervalues set a minimum wait. A server-requested wait above 60 seconds ends automatic retrying and returns the error for the application to handle later. - Retries rate-limit HTTP 429 responses, excluding billing/quota exhaustion. Retries HTTP 500,
502, 503, and 504 only for
GET,PUT, andDELETE, using the effective method after custom cURL options.POSTandPATCHare not retried on server errors. - Disables retries for streaming, response-body/upload/read callbacks, custom output files, and
requests with
CURLOPT_RETURNTRANSFERdisabled. These transfers cannot safely be replayed.
Responses API
$result = $open_ai->createResponse([
"model" => "gpt-4o-mini",
"input" => "Hello",
"max_output_tokens" => 100,
]);
Token counting and explicit compaction use countResponseInputTokens($opts) and compactResponse($opts).
File inputs, tools, structured output, conversation state, automatic compaction, and background mode are
passed through as request options; see API_COVERAGE.md.
To resume a response created with background => true and stream => true:
$open_ai->retrieveResponse($responseId, [
'stream' => true,
'starting_after' => $lastSequenceNumber,
], function ($curl, $data) {
echo $data;
return strlen($data);
});
For the Responses multi-agent HTTP beta, add its header with
$open_ai->setHeader(['OpenAI-Beta' => 'responses_multi_agent=v1']) and pass the documented
multi_agent options to createResponse(). The application handles tool execution and SSE events.
Conversations API
$result = $open_ai->createConversation();
Agents API
Run hosted agents in sandboxed environments. The Agents API is in beta. Every method sends the
OpenAI-Beta: agents=v1 header for that request only, and your own headers are restored afterwards.
See the Agents guide for models, environments, and tools.
With environment.type set to openai_hosted, OpenAI runs the sandbox. Self-hosted environments
require your own executor; custom function tools are handled by your application.
Create a session and read the result
Create a session with stream enabled and pass a callback. It receives raw server-sent event chunks and
must return the number of bytes it handled:
$open_ai->createAgentSession([
'agent' => [
'model' => 'gpt-6-astra',
'instructions' => 'Write clean code, run it, and report the actual output.',
],
'environment' => ['type' => 'openai_hosted'],
'input' => 'Create tree.py, a script that prints the files in this directory. Run it and show the output.',
'stream' => true,
], function ($curl, $chunk) {
echo $chunk;
return strlen($chunk);
});
For follow-up input, create an idle session and share its ID with a subscriber and a sender:
$session = json_decode($open_ai->createAgentSession([
'agent' => ['model' => 'gpt-6-astra'],
'environment' => ['type' => 'openai_hosted'],
]), true, 512, JSON_THROW_ON_ERROR);
$sessionId = $session['id'];
echo $sessionId . PHP_EOL;
Run the subscriber in a separate process with this ID. Wait until its event-stream connection is established before running the sender. Streams deliver live events and do not replay missed events:
$sessionId = getenv('OPENAI_AGENT_SESSION_ID'); // ID from session creation
$open_ai->streamAgentSessionEvents($sessionId, function ($curl, $chunk) {
echo $chunk;
return strlen($chunk);
});
Send input and approve actions
Run this sender in another process after the subscriber connects. The body is {"events": [...]}.
$sessionId = getenv('OPENAI_AGENT_SESSION_ID');
$open_ai->sendAgentSessionEvents($sessionId, [
[
'type' => 'agent.session.input.message',
'input' => [[
'role' => 'user',
'content' => [['type' => 'input_text', 'text' => 'Now add a --depth option.']],
]],
],
]);
Other input events include agent.session.input.cancel, agent.session.input.tool_result, and
agent.session.input.computer_use_approval_request_result. See the
session events guide for payloads.
Manage sessions
$open_ai->listAgentSessions(['limit' => 20, 'order' => 'desc']);
$open_ai->retrieveAgentSession($sessionId);
$open_ai->updateAgentSession($sessionId, ['agent' => ['reasoning' => ['effort' => 'low']]]);
$open_ai->listAgentSessionItems($sessionId, ['order' => 'asc', 'limit' => 100]);
$open_ai->listAgentSessionTurns($sessionId, ['limit' => 20]);
$open_ai->retrieveAgentSessionTurn($sessionId, 'turn_123');
$open_ai->exportAgentSessionTraces($sessionId, ['limit' => 20]); // OTLP JSON; needs trace export enabled
$open_ai->deleteAgentSession($sessionId); // cancel active execution before deleting
Environments
Prewarming requires access to the prewarming beta. Prepare a workspace before attaching it to a session; unattached environments expire after five minutes. Supply any files the task needs:
$environment = json_decode($open_ai->createAgentEnvironment([
'environment' => [
'type' => 'openai_hosted',
'packages' => ['python' => ['pandas==2.2.3']],
'files' => [[
'type' => 'inline',
'path' => '/workspace/data.csv',
'data' => base64_encode("category,amount\nbooks,10\nbooks,20\n"),
]],
],
]), true, 512, JSON_THROW_ON_ERROR);
$open_ai->createAgentSession([
'agent' => ['model' => 'gpt-6-astra'],
'environment' => ['type' => 'openai_hosted', 'environment_id' => $environment['id']],
'input' => 'Use pandas to summarize /workspace/data.csv.',
'stream' => true,
], function ($curl, $chunk) {
echo $chunk;
return strlen($chunk);
});
$open_ai->listAgentEnvironments();
$open_ai->retrieveAgentEnvironment($environment['id']);
Request bodies are passed through unchanged, so new beta fields work without an SDK update. Session IDs are URL-encoded for you. Model names, environment options, and event types change during the beta; check the guide for current values.
Vector Stores API
$result = $open_ai->createVectorStore([
"name" => "Support FAQs"
]);
Batches API
$upload = $open_ai->uploadFile([
'purpose' => 'batch',
'file' => new CURLFile('/path/to/requests.jsonl', 'application/jsonl', 'requests.jsonl'),
]);
$batchFileId = json_decode($upload, true, 512, JSON_THROW_ON_ERROR)['id'];
$result = $open_ai->createBatch([
"completion_window" => "24h",
"endpoint" => "/v1/responses",
"input_file_id" => $batchFileId,
]);
Uploads API
Create an upload, add its bytes as multipart parts, then complete it with the ordered part IDs. Each part must be at most 64 MB. This example uploads one part:
$path = '/path/to/document.txt';
$upload = json_decode($open_ai->createUpload([
'filename' => 'document.txt',
'purpose' => 'assistants', // Accepted for vector store files.
'bytes' => filesize($path),
'mime_type' => 'text/plain',
]), true, 512, JSON_THROW_ON_ERROR);
$part = json_decode($open_ai->addUploadPart($upload['id'], [
'data' => new CURLFile($path, 'application/octet-stream', 'part.bin'),
]), true, 512, JSON_THROW_ON_ERROR);
$result = $open_ai->completeUpload($upload['id'], ['part_ids' => [$part['id']]]);
Realtime API
Create a short-lived client secret with a nested session configuration:
$result = $open_ai->createRealtimeClientSecret([
'session' => [
'type' => 'realtime',
'model' => 'gpt-realtime-2.1-mini',
'instructions' => 'You are a helpful assistant.',
],
'expires_after' => ['anchor' => 'created_at', 'seconds' => 600],
]);
createRealtimeTranslationClientSecret() issues translation session credentials.
createRealtimeCall() accepts an SDP offer and a session array, returning the raw SDP answer.
acceptRealtimeCall(), hangupRealtimeCall(), referRealtimeCall(), and rejectRealtimeCall()
control existing calls. These REST wrappers do not implement a WebRTC media or WebSocket client.
See the client-secret API reference.
Decisions API
$result = $open_ai->createDecision([
'model' => 'gpt-6-luna',
'input' => 'The support desk mascot is a purple owl.',
'questions' => [[
'type' => 'predicate',
'name' => 'owl',
'instructions' => 'The mascot is an owl.',
]],
]);
See the Decisions guide for beta availability and question types.
Webhooks
Use createWebhookEndpoint(), listWebhookEndpoints(), retrieveWebhookEndpoint(),
updateWebhookEndpoint(), deleteWebhookEndpoint(), rotateWebhookEndpointSecret(), and
testWebhookEndpoint() to manage subscriptions. Testing an endpoint sends an event to its configured URL.
These methods use /v1/webhook_endpoints.
Verify incoming events using the original request body and the webhook signing secret:
use Orhanerday\OpenAi\Webhook;
$event = Webhook::unwrap(
file_get_contents('php://input'),
getallheaders(),
getenv('OPENAI_WEBHOOK_SECRET')
);
Verification throws on invalid signatures, missing headers, or timestamps outside the default five-minute tolerance. Your application should handle retries idempotently using the webhook ID. See the webhooks guide.
Usage and Costs
Use an organization admin API key for these read-only reporting endpoints:
$reporting = new OpenAi(getenv('OPENAI_ADMIN_KEY'));
$options = [
'start_time' => time() - 7 * 86400,
'end_time' => time(),
'bucket_width' => '1d',
'group_by' => ['model', 'project_id'],
'batch' => false,
'limit' => 7,
];
$page = json_decode($reporting->getCompletionsUsage($options), true, 512, JSON_THROW_ON_ERROR);
if ($reporting->getCURLInfo()['http_code'] !== 200) {
throw new RuntimeException('Usage request failed; check the decoded API error.');
}
// Each entry in data is a time bucket containing aggregated results.
foreach ($page['data'] as $bucket) {
foreach ($bucket['results'] as $result) {
echo $result['input_tokens'] . PHP_EOL;
}
}
// To fetch the next page, preserve the filters and pass the returned cursor.
if ($page['has_more']) {
$options['page'] = $page['next_page'];
$nextPage = $reporting->getCompletionsUsage($options);
}
All methods require an options array with start_time (Unix seconds). Filters, array-valued
group_by, time buckets, limits, and the page cursor pass through as GET query parameters.
Each method returns the raw response body; pagination is controlled by the caller.
| Report | Method |
|---|---|
| Completions | getCompletionsUsage($options) |
| Decisions | getDecisionsUsage($options) |
| Embeddings | getEmbeddingsUsage($options) |
| Moderations | getModerationsUsage($options) |
| Images | getImagesUsage($options) |
| Audio speeches | getAudioSpeechesUsage($options) |
| Audio transcriptions | getAudioTranscriptionsUsage($options) |
| Vector stores | getVectorStoresUsage($options) |
| Code interpreter sessions | getCodeInterpreterSessionsUsage($options) |
| File search calls | getFileSearchCallsUsage($options) |
| Web search calls | getWebSearchCallsUsage($options) |
| Costs | getCosts($options) |
For costs, use the same time-range and pagination fields, with cost-specific grouping:
$costs = $reporting->getCosts([
'start_time' => time() - 7 * 86400,
'bucket_width' => '1d',
'group_by' => ['project_id', 'line_item'],
]);
Costs supports daily buckets and should be used for spend reporting. Usage supports minute,
hour, and day buckets; usage aggregates may differ from billing totals. Filters and grouping
fields vary by endpoint. See the official Usage and Costs reference.
The /organization/usage/completions reporting route remains supported independently of
the removed legacy text-generation integration.
Testing
Composer selects a compatible Pest version for your PHP runtime. The library continues to support PHP 7.4+.
Run the actual API tests with a key from the environment or your local .env:
export OPENAI_API_KEY='your-api-key'
composer test
composer test-live runs only live API tests. Without a key, API tests are explicitly skipped.
Usage and Costs tests require OPENAI_ADMIN_KEY; run them separately with
vendor/bin/pest tests/UsageLiveTest.php. Without that admin key, those cases are explicitly skipped.
To run native cURL transport and webhook signature checks without API calls:
vendor/bin/pest --exclude-group=live
There are no cURL mocks. Live tests may incur API charges. They create and delete their own files, Responses, conversations, stored chats, uploads, and vector stores. They never launch a training job. Stored chat tests poll briefly because persistence can be asynchronous.
Models can be selected with OPENAI_CHAT_MODEL, OPENAI_IMAGE_MODEL, OPENAI_COMPACTION_MODEL,
OPENAI_DECISION_MODEL, and OPENAI_REALTIME_MODEL. Defaults follow the examples and current guides.
Additional account-dependent tests and required fixtures are documented in API_COVERAGE.md.
Push/PR CI runs native transport and signature checks on PHP 7.4 and 8.4; manually dispatch the Tests
workflow with an OPENAI_API_KEY repository secret to run the live suite. Add the optional
OPENAI_ADMIN_KEY secret to include organization Usage and Costs tests.
Changelog
See CHANGELOG.md.
Contributing
See CONTRIBUTING.md.
Security Vulnerabilities
Report security vulnerabilities to orhanerday@gmail.com
Credits
License
MIT.