Build an AI Chatbot in PHP Using OpenAI API: Step-by-Step Tutorial

August 31, 2026, 5:30 AM

Build an AI Chatbot in PHP Using OpenAI API: Step-by-Step Tutorial

Developers keep putting off this one thing because it feels too complex - AI chatbot creation with PHP. Just because they feel they might get lost in Python, TensorFlow, data science background (or at the very least a weekend worth of configuration). None of that is true. If you're able to write a cURL request in PHP, you already have the main skill. The OpenAI API does the actual intelligence work. Your job is just to wire things up correctly.

This guide walks through the complete ai chatbot creation with PHP process, from installing the library to building a streaming web interface, based on real working implementations. Every step exists for a reason, and those reasons get explained alongside the code.

What You Need to Begin

The requirements for PHP for ai chatbot development are refreshingly short. You need PHP 8.1 or higher with Composer installed, and an OpenAI API key from platform.openai.com. That is genuinely all.

Start by pulling in the official OpenAI PHP client:

composer require openai-php/client

This library wraps the API cleanly and handles streaming out of the box, which saves you a lot of manual work later. Before writing a single line of chatbot logic though, get your API key stored correctly. This step sounds boring. Skip it and you will regret it.

Never put your API key directly in a PHP file. Set it as a server environment variable and read it with getenv('OPENAI_API_KEY'). If a hardcoded key ever touches version control, even for ten minutes, then automated bots will find it. There are documented cases of developers finding thousands of dollars in OpenAI charges within hours of accidentally pushing a key to a public GitHub repo. If you prefer a config file approach, keep it outside your web root and away from anything Composer or Git touches:

<?php

// config.php — lives OUTSIDE public_html, never committed

return [

    'openai_api_key' => 'sk-your-key-here',

    'openai_model'   => 'gpt-4o-mini',

    'max_tokens'     => 1000,

    'temperature'    => 0.7,

];

The temperature value is worth understanding before you move on. It controls how creative or focused the AI sounds. Make it closer to 0 and you become far more specific, with responses that seem almost factual. Advance it towards 1 and the AI becomes more diverse and creative. Let's consider the ideal for a customer support chatbot - 0.5 to 0.7 is the sweet spot! For something more creative, get up higher.

Building the Non-Streaming Backend

At the heart of the AI chatbot creation process is a simple PHP file that receives a message from the user, sends this message to OpenAI with the full history of the conversation and returns a response. This is the full working version:

<?php

require 'vendor/autoload.php';

session_start();

header('Content-Type: application/json');

$input = json_decode(file_get_contents('php://input'), true)['message'] ?? '';

if (!isset($_SESSION['history'])) {

    $_SESSION['history'] = [

        ['role' => 'system', 'content' => 'You are a helpful assistant.']

    ];

}

$_SESSION['history'][] = ['role' => 'user', 'content' => $input];

$client   = OpenAI::client(getenv('OPENAI_API_KEY'));

$response = $client->chat()->create([

    'model'    => 'gpt-4o-mini',

    'messages' => array_slice($_SESSION['history'], -20),

]);

 

$reply = $response->choices[0]->message->content;

$_SESSION['history'][] = ['role' => 'assistant', 'content' => $reply];

echo json_encode(['reply' => $reply]);

Every line here is doing deliberate work. session_start() is what gives your chatbot memory across requests. Otherwise, every message that the user sends hits as a fresh conversation, with no contextual background. The user might say "I said my order number before," and the AI would be utterly confused, because from an API perspective there is no "earlier".

The array_slice($_SESSION['history'], -20) part caps how much history you send with each request. This is your primary cost control lever. The longer the conversation, the more tokens you consume per request, because the entire history travels to the API every single time. Capping at 20 messages keeps costs predictable without making the chatbot feel amnesiac.

The system message sitting at the top of the history array is where you define what your chatbot actually is. Right now it says "You are a helpful assistant." Change that one line to "You are a customer support agent for a furniture store. Always recommend checking the warranty page before suggesting replacements." and the entire personality and behavior of the chatbot shifts instantly. No other code changes needed.

The Thing Most Tutorials Do Not Explain Properly

Here is the part that trips up most developers building their first chatbot: the OpenAI API has no memory at all between requests. Zero. Every call is completely isolated.

This means the AI chatbot creation process has a hidden responsibility that is easy to miss. You are not just sending a message. You are responsible for sending the entire conversation history with every single request, every single time. The API reads that history, understands the context, and responds accordingly. If you forget to send the history, or you send it incorrectly, the AI loses the thread of the conversation entirely.

The session approach in the code above handles this properly. Each user message gets appended to $_SESSION['history']. The full history goes out with each API call. The AI reply gets appended back. The session persists as long as the browser session is active.

One thing to be careful about when trimming history: always keep the system message intact. If your trim accidentally removes the system message from position zero, the chatbot loses its configured role and reverts to acting like a generic assistant. When you slice the history, preserve the first element separately, trim the rest, then put the system message back at the top.

Adding Streaming So Responses Feel Alive

And after this traditional fashion, the API doesn't even send a single byte back until it has built up the entire response. But if it is a two-sentence response, then this will do. For anything longer, the user sits staring at a blank screen for seconds, which also feels wasted even when everything is working as intended. 

Streaming fixes this. Rather than waiting for the full response, each chunk of words is streamed to the browser as it is generated. The reply gets built up word by word in front of the user, just like ChatGPT. It makes the wait, which is not great, into something gratifying.

Here is the streaming backend:

<?php

require 'vendor/autoload.php';

session_start();

header('Content-Type: text/event-stream');

header('Cache-Control: no-cache');

 

$input = json_decode(file_get_contents('php://input'), true)['message'] ?? '';

$_SESSION['history'][] = ['role' => 'user', 'content' => $input];

$stream = OpenAI::client(getenv('OPENAI_API_KEY'))->chat()->createStreamed([

    'model'    => 'gpt-4o-mini',

    'messages' => $_SESSION['history'],

]);

 

$full = '';

foreach ($stream as $response) {

    $delta = $response->choices[0]->delta->content;

    if ($delta !== null) {

        $full .= $delta;

        echo "data: " . json_encode(['chunk' => $delta]) . "\n\n";

        ob_flush();

        flush();

    }

}

$_SESSION['history'][] = ['role' => 'assistant', 'content' => $full];

echo "data: [DONE]\n\n";

Content-Type: text/event-stream header is for leaving that connection set to Server-Sent Events mode. createStreamed() calls the OpenAI client so that it streams the response back instead of completing. We'll receive each chunk in the foreach loop and immediately send it through to the browser using ob_flush() and flush(). The $full variable assembles the complete response in the background so the finished message can be saved to session history once streaming completes. Without that assembly step, your conversation history would only contain fragments.

The Frontend That Reads the Stream

The JavaScript side needs to read each chunk as it arrives and append it to the output. Here is a clean implementation using the Fetch API:

const res = await fetch('stream.php', {

    method: 'POST',

    headers: { 'Content-Type': 'application/json' },

    body: JSON.stringify({ message: text }),

});

 

const reader  = res.body.getReader();

const decoder = new TextDecoder();

let buffer    = '';

 

while (true) {

    const { done, value } = await reader.read();

    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    for (const line of buffer.split('\n')) {

        if (!line.startsWith('data: ')) continue;

        const data = line.slice(6);

        if (data === '[DONE]') return;

        const { chunk } = JSON.parse(data);

        outputEl.textContent += chunk;

    }

}

The buffer variable is the detail most beginner implementations skip, and it is the reason those implementations break intermittently. Network chunks do not always arrive cleanly aligned with SSE message boundaries. A chunk can arrive mid-line, splitting a JSON payload across two network reads. Buffering the incoming data and splitting on newlines handles this correctly regardless of how the network delivers the bytes.

Production Checklist Before You Go Live

Getting the chatbot working in development and keeping it stable under real traffic are genuinely different problems. Before any ai chatbot creation with PHP project goes live, work through these:

Set OPENAI_API_KEY as a server environment variable, never hardcoded anywhere. Run everything over HTTPS sessions and Server-Sent Events both have security implications over plain HTTP. Trim $_SESSION['history'] consistently, always preserving the system message. Add per-user rate limiting so one power user does not drain your entire API budget in an afternoon. Catch both ErrorException and TransporterException from the OpenAI PHP client - both occur in production environments and both will surface unhandled stack traces to users if you leave them unwrapped.

On costs: every API response includes a usage field showing prompt tokens, completion tokens, and total tokens consumed. Log this data per session from the start. You will see runaway conversations and strange usage patterns well before they show up as a surprise on the bill.

Frequently Asked Questions (FAQs)

1. Why does the AI seem to forget what the user said earlier? 

The conversation history is either not being sent with each request or not being appended correctly after each exchange. Every assistant reply must be added to the history array before the next API call goes out, not after.

2. Which model should you use? 

Both gpt-4o-mini and gpt-4.1-mini solve the vast majority of chatbot use cases very well and at a small fraction of the cost of full GPT-4. Take one of these and only upgrade if you hit an actual capability ceiling, not when it seems like it could maybe be smarter.

3. Does this work inside a Laravel application? 

Completely. Store the API key in .env and read it with env('OPENAI_API_KEY'). Replace $_SESSION with Laravel's session helpers. The OpenAI client and all the logic around it work identically inside a Laravel controller.

 

 

Recent Blogs