Call OpenAI API From PHP Without an SDK

Call the OpenAI API from PHP using cURL and JSON, with secure API key storage, WordPress plugin integration, response handling, and HTTP error checks.

WordPress plugin using PHP cURL to send a server-side request to the OpenAI API and receive a JSON response without an SDK.

The short answer: to call the OpenAI API from PHP without an SDK, send a POST request with PHP’s built-in cURL extension to https://api.openai.com/v1/chat/completions, with an Authorization: Bearer header carrying a key read from an environment variable, and a JSON body built with json_encode(). No Composer package is required. What takes longer to get right is where that code lives in a WordPress codebase, how the key is stored, and what happens when the request fails. The rest of this page covers those parts.

Why Skip the SDK at All

The official and community PHP clients for OpenAI wrap the same HTTP call this page shows: a POST with a JSON body and a bearer token. Pulling in a package through Composer is reasonable on a project that already manages dependencies that way, but plenty of WordPress work does not. A single cURL call adds no dependency, nothing to update when the SDK ships a breaking change, and no extra code you did not write yourself to audit. The tradeoff is that you own the retry logic and error parsing the SDK would otherwise handle, which the sections below walk through.

Where This Code Belongs in a WordPress Site

The most common way this goes wrong has nothing to do with the API call itself. It is putting the call, the key, and the request logic into functions.php, because that is where the first snippet-site example told someone to paste it.

functions.php runs on every single request to the site, front end and admin, before most of WordPress has finished loading. A typo in a file that size does not break one feature, it breaks the whole site, including the admin screen you would use to fix it. Why editing functions.php keeps destroying your site covers exactly this failure mode and what recovering from it involves. An API integration, with its external request, its error handling, and its own settings, is exactly the kind of code that should not depend on the active theme staying untouched. Build it as a small plugin instead. Your first real plugin: headers, structure and not using functions.php covers the plugin header WordPress needs and a file layout that survives the project growing past one function.

Get the API Key and Keep It Out of the Codebase

Create an API key from the OpenAI account dashboard. Treat it the same way you would a database password: it is a secret that grants access billed to whoever created it, and it should never appear in a file that gets committed to version control or pasted into a support ticket.

On a typical WordPress host, the cleanest place to set it is a server-level environment variable, read in PHP with getenv(). Where the host does not give you that option, a constant defined in wp-config.php, which already sits outside the plugin directory and is excluded from most deploy pipelines, is the next best option, read with defined('OPENAI_API_KEY') ? OPENAI_API_KEY : null. Either way, the key is never written directly into the plugin file that makes the request, and it is never sent to the browser. If a page needs the model’s answer, the browser calls your WordPress endpoint, and your server calls OpenAI, never the other way round.

How to Call the OpenAI API From PHP With cURL

Diagram showing how to call the OpenAI API from PHP using cURL, from the WordPress server to api.openai.com and back with the JSON response

Here is the complete request, with the key read from the environment rather than typed inline:

<?php
$apiKey = getenv('OPENAI_API_KEY');
if ($apiKey === false) {
    // Fail loudly here rather than sending an empty Authorization header.
    return new WP_Error('missing_api_key', 'OPENAI_API_KEY is not set.');
}
$payload = [
    'model'    => 'gpt-4o-mini',
    'messages' => [
        ['role' => 'user', 'content' => $userPrompt],
    ],
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $apiKey,
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

A few details matter more than they look. CURLOPT_RETURNTRANSFER makes curl_exec() return the response body as a string instead of printing it straight to output, which is what lets you decode and inspect it before anything reaches the page. CURLOPT_HTTPHEADER replaces the whole header array each time it is called, so if you need to add a header later, add it to this same array rather than calling curl_setopt() a second time with just the new one, per the PHP manual for curl_setopt (read 2026-09-18). And always set a timeout: an LLM request can take noticeably longer than a typical API call, and an uncapped one can hold a PHP worker until the host kills it.

Reading the Response and Handling Errors

Decode the body and check the shape before touching any field:

$data = json_decode($response, true);
if ($statusCode !== 200) {
    $errorMessage = $data['error']['message'] ?? 'Unknown error';
    // Log $statusCode and $errorMessage; do not show either to the visitor as-is.
}
$reply = $data['choices'][0]['message']['content'] ?? null;

Not every non-200 status should be retried the same way. OpenAI’s own API reference (read 2026-09-18) documents bearer-token authentication and states that your key should be loaded from an environment variable or a key management service on the server, never exposed in client-side code.

$statusCodeWhat it usually meansWhat your PHP code should do
200Request succeededRead choices[0].message.content
401Key missing, malformed, or revokedFix the key; do not retry
429, with a Retry-After headerTemporary rate limitWait at least that long, then retry a bounded number of times
429, without Retry-After, tied to a spend or usage limitBudget or quota reachedAlert a human; retrying will not help
500 / 503Provider-side troubleRetry with backoff, bounded

Check for a Retry-After header when one is present and respect it rather than guessing a wait time, and treat any spend or usage limit response as something for a human to act on, not something to loop on.

If This Runs Behind an AJAX Endpoint, Secure It Properly

Most WordPress use cases put this call behind admin-ajax.php or a custom REST route, triggered by a button on a page. That endpoint needs two separate checks, and they answer different questions. A nonce, verified with wp_verify_nonce() or check_ajax_referer(), confirms the request came from a form your site actually served recently. It says nothing about whether the person submitting it is allowed to trigger a paid API call. That second question is a capability check, current_user_can(), and it is the one that is missing more often than the nonce is. Nonces are not authentication: what they actually protect sets out why an endpoint needs both, not either, before it goes anywhere near a request that costs money per call.

Never Echo the Model’s Output Unescaped

Whatever comes back in choices[0].message.content is text you did not write and did not review before it reached your server. Treat it exactly like any other value a stranger submitted: it gets escaped for the context it is about to appear in, whether that is esc_html() for a page, esc_js() for inline script data, or nothing executed and nothing inserted into a database query unchecked. Sanitize, escape, validate: which one, where, and why the order matters is the reference for picking the right function for each of those moments, and the model’s response is not an exception to that rule just because it came from an API instead of a form field.

Testing Before You Wire It Into a Page

Before pointing a button at this code, run the same request from a terminal with curl directly, swapping in the key as an environment variable there too. If the command-line request succeeds and the PHP version does not, the difference is almost always in the header array or the JSON encoding, not in cURL itself, and isolating that difference before debugging the WordPress plugin around it saves the most time.

What This Setup Does Not Cover

This is the minimum request-and-key handling, not a production integration. It has no retry logic beyond the single attempt shown, no streaming support for showing a response as it arrives, and no queueing for a page that needs to call the model for many visitors at once. A single synchronous cURL call also ties up a PHP worker for as long as the request takes, which is fine for a low-traffic admin tool and the wrong design for a public-facing feature under real load. None of that changes the request format above, but a feature going into production needs a plan for what happens when a request is slow, when the same prompt arrives twice in a row, and when the model’s usual latency spikes during a provider incident. Building that plan is worth doing before the feature ships, not after the first slow page load reaches a real visitor.

FAQ

Do I need Composer or an SDK to call the OpenAI API from PHP? No. PHP’s built-in cURL extension, enabled on nearly every host, is enough to send the request and read the response. An SDK is a convenience, not a requirement.

Where should I store the OpenAI API key in a WordPress project? In a server-level environment variable read with getenv(), or a constant in wp-config.php if the host does not support environment variables. Never in a plugin file that gets committed to version control, and never sent to the browser.

Why is my request failing with a 401 error? A 401 means the key is missing, mistyped, or has been revoked. Print the key value right before the request (to your logs, not the page) and compare it character for character with the value on the OpenAI dashboard.

Should my PHP code retry every failed request automatically? No. Retry a temporary rate-limit response, honoring a Retry-After header when the provider sends one. Do not retry a response tied to an authentication problem or a spend or usage limit; those need a fix, not another attempt.

Can I call the OpenAI API directly from JavaScript in the browser instead of PHP? Not safely. The request needs your API key, and any key shipped to the browser is readable by every visitor who opens developer tools. Keep the call server-side, in PHP, and have the browser talk to your own endpoint instead.

Written by

Shah Alom

Shah Alom is the founder and writer behind Ebuhu, where he covers PHP, WordPress development, plugin and theme development, debugging, practical programming techniques, and AI-assisted coding. Drawing on hands-on web development experience, he focuses on clear, practical guidance that helps developers understand how things work, avoid common mistakes, and write more reliable code.

Leave a Reply

Your email address will not be published. Required fields are marked *