PHP
PHP chat API with no WebSocket server to run
Your PHP app stays a normal request-response app. It signs a token for the logged-in user and syncs users over our REST API, with no chat package to install. The browser loads the chat UI from a CDN, and we run the real-time part: we store and deliver the messages and send the notifications.
.message {
white-space: normal;
overflow: hidden;
border-radius: 0.75rem;
border-width: 1px;
border-style: solid;
word-wrap: break-word;
position: relative;
display: inline-block;
max-width: calc(100% - 6rem - 0.25rem - 0.25rem);
border-color: #F2F2F2;
background-color: #F2F2F2;
color: #171717;
}
.has-avatar-spacer .message {
max-width: calc(100% - 3rem - 0.25rem - 0.25rem);
}
.by-me .message {
border-color: #171717;
background-color: #171717;
color: #fff;
margin-right: 0.50rem;
}


Who does what in your PHP chat
Here's the split. Only the first card is server code you write.
Signs a token for the logged-in user with your secret key, and creates users and conversations through the REST API. It all happens inside your normal requests, so there's no extra process to run.
Loads the chat UI from the jsDelivr CDN as a web component and connects as the user your template names.
Runs the real-time connections, stores message history, and sends email, SMS and push notifications. It calls your PHP endpoint with signed webhooks when messages are sent or read.
Add chat to a PHP page in four steps
Everything below is plain PHP and HTML. The only thing you install is a JWT library.
01
Put the chat in your PHP template
Add the stylesheet, the import map and one module script to the <head> of your layout. Then place a <t-chatbox> element where the chat should appear, with the logged-in user's ID echoed into user-id. It's our pre-built chat UI, loaded from the jsDelivr CDN.
<!-- In the <head> of your layout -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@talkjs/web-components@<COMPONENTS_VERSION>/default.css"
/>
<script type="importmap">
{
"imports": {
"@talkjs/web-components": "https://cdn.jsdelivr.net/npm/@talkjs/web-components@<COMPONENTS_VERSION>",
"@talkjs/core": "https://cdn.jsdelivr.net/npm/@talkjs/core@<CORE_VERSION>"
}
}
</script>
<script type="module" async>
import "@talkjs/web-components";
</script>
<!-- Where the chat goes -->
<t-chatbox
style="width: 400px; height: 600px;"
app-id="<APP_ID>"
user-id="<?= htmlspecialchars($user['id']) ?>"
conversation-id="<?= htmlspecialchars($conversationId) ?>"
></t-chatbox>Replace <COMPONENTS_VERSION> and <CORE_VERSION> with the current npm versions of @talkjs/web-components and @talkjs/core, and <APP_ID> with the app ID from the Settings page of your TalkJS dashboard. In Laravel, a Blade view echoes {{ auth()->id() }} the same way, and we have a separate page on Laravel chat with TalkJS. The JavaScript chat UI docs cover every option.
02
Sign the user's token in PHP
With authentication on, only users with a valid token can connect, so nobody can read or send messages as someone else. Sign the token on your server with the lcobucci/jwt library (composer require lcobucci/jwt). This is the PHP code from our docs: a JWT with tokenType set to user, your app ID as issuer, the user ID as subject and a one-day expiry, signed with HS256 and your secret key.
<?php
// Uses `lcobucci/jwt`: https://packagist.org/packages/lcobucci/jwt
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Token\Builder;
require 'vendor/autoload.php';
$tokenBuilder = new Builder(new JoseEncoder(), ChainedFormatter::default());
$algorithm = new Sha256();
$signingKey = InMemory::plainText("<SECRET_KEY>_GOES_HERE_REPLACING_THIS_TEXT");
$now = new DateTimeImmutable();
$token = $tokenBuilder
->withClaim('tokenType', 'user')
->issuedBy('<APP_ID>')
->relatedTo('<USER_ID>')
->expiresAt($now->modify('+1 days'))
->getToken($algorithm, $signingKey)
->toString();
echo $token;Run it in your controller before the template renders. Use the logged-in user's ID where <USER_ID> is, and keep the result in $authToken instead of echoing it. Keep your secret key in an environment variable on the server and never put it in template output: it gives full admin access to your TalkJS account.
Then pass the token to the page with setToken, once, before the chat mounts. Replace the module script from step 1 with this one:
<script type="module" async>
import "@talkjs/web-components";
import { getTalkSession } from "@talkjs/core";
getTalkSession({
appId: "<APP_ID>",
userId: <?= json_encode((string) $user['id']) ?>,
}).setToken("<?= $authToken ?>");
</script>When the chat loads without errors, turn on authentication (identity verification) under Security settings on the Settings page of your dashboard. New accounts start with it off. The example token expires after 24 hours, so for longer sessions, add a normal PHP endpoint that returns a fresh token. The authentication docs show how to do both.
03
Sync users and conversations over REST
Your users and conversations need to exist in TalkJS before they can chat. Create them from PHP with the REST API at https://api.talkjs.com. REST calls need an admin token: the same JWT setup, with tokenType set to admin and no subject. Our docs advise a new token for each request, valid for 30 seconds:
<?php
// Uses `lcobucci/jwt`: https://packagist.org/packages/lcobucci/jwt
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Token\Builder;
require 'vendor/autoload.php';
$tokenBuilder = new Builder(new JoseEncoder(), ChainedFormatter::default());
$algorithm = new Sha256();
$signingKey = InMemory::plainText("<SECRET_KEY>_GOES_HERE_REPLACING_THIS_TEXT");
$now = new DateTimeImmutable();
$token = $tokenBuilder
->withClaim('tokenType', 'admin')
->issuedBy('<APP_ID>')
->expiresAt($now->modify('+30 seconds'))
->getToken($algorithm, $signingKey)
->toString();
echo $token;Wrap that code in a function, talkjs_admin_token(), that returns $token. Send the token as Authorization: Bearer, not the secret key itself, which is the legacy way. Then create or update the user with PUT /v1/{appId}/users/{userId} and the conversation with PUT /v1/{appId}/conversations/{conversationId}. Both calls are idempotent, so your login code can make them every time. Here's an example with curl:
<?php
// Example: TalkJS REST calls with curl.
// talkjs_admin_token() is the admin token code above, returning $token.
function talkjs_put(string $path, array $body): int
{
$ch = curl_init('https://api.talkjs.com/v1/<APP_ID>' . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . talkjs_admin_token(), // new token per request
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
return curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 200 means success
}
// On login: create or update the user, then the conversation.
talkjs_put('/users/' . rawurlencode($user['id']), [
'name' => $user['name'],
'email' => [$user['email']],
]);
talkjs_put('/conversations/' . rawurlencode($conversationId), [
'participants' => [(string) $user['id'], (string) $otherUserId],
]);TalkJS returns HTTP 200 when a call succeeds, so check the status code, not the response body. If you sync all users and conversations over REST, you can turn off browser synchronization.
04
Receive webhooks in PHP
Turn on webhooks on the Settings page of your dashboard, and TalkJS sends your PHP endpoint a POST request with a JSON payload for events such as message.sent and message.read. Check the X-TalkJS-Signature header before you trust a request. It's an HMAC-SHA256 of the X-TalkJS-Timestamp value, a dot and the raw request body, in uppercase hex, signed with your secret key marked "use for webhooks".
<?php
// Example: a PHP endpoint that receives TalkJS webhooks.
$payload = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_TALKJS_SIGNATURE"] ?? "";
$timestamp = $_SERVER["HTTP_X_TALKJS_TIMESTAMP"] ?? "";
$secret = getenv("TALKJS_SECRET_KEY"); // your secret key marked "use for webhooks"
$expectedSignature = strtoupper(hash_hmac("sha256", "$timestamp.$payload", $secret));
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($payload);
if ($event->type === "message.sent") {
// Store $event->data->message, or queue slower work for later.
}
http_response_code(200);By default, TalkJS waits 5 seconds for a response, so answer 200 quickly and do slow work later. The code builds on our vanilla PHP webhook example on GitHub, which also shows the Laravel version of the lines that read the request.
We were able to offload all messaging to TalkJS — hugely simplifying our development of an otherwise complex stack.
Ready-made chat UI you can customize
The chat UI comes with three different layouts—that are highly customizable—which you can mix and match to suit your needs. You have complete control over the styling, the layout, and document structure of the chat messages and the possibility to show metadata.
The TalkJS Inbox shows the chat history on the left, selected conversation on the right. It's designed to act as the messaging center of your app, usually living on its own page.
The TalkJS Chatbox is the right half of the Inbox UI mode. It takes up less space and is designed to be embedded in your app near an order confirmation, a booking, or user profile.
You can also show a conversation in a popup that only shows up when the user clicks a button somewhere. When shown, the popup is positioned on the bottom right of your page.
What you'd run yourself to build chat in PHP
A classic PHP script runs per request, sends its output and closes the connection. Live chat needs a WebSocket server that runs all the time and keeps connections open. With Laravel Reverb, that usually means a process manager such as Supervisor, a reverse proxy and a restart whenever your code changes. Past about 1,000 connections you need a different event loop than the default one, and to spread across several servers you need a central Redis server. Broadcasting events also needs a queue worker.
And that server only moves events. You'd still build the chat UI, history, unread counts, notifications and moderation on top of it. We've built all of those. Your PHP app stays request-response, you can be live in days, and our pricing counts monthly active users only.