livqenoDocs

SDKsPHP

PHP SDK

Make tokens and manage rooms, chat and streams from your PHP backend. PHP 8.1+, ext-curl only.

livqeno/livqeno-php is for your PHP backend (Laravel, Symfony, or a plain script behind PHP-FPM). It holds your secret API key and talks to the Livqeno REST API for you.

Its main job is making tokens. Your app (web, React, React Native, Flutter, iOS or Android) can't join a call or chat without one, and only a backend holding the API key can make one. In a typical app, this SDK sits behind an endpoint like POST /join-room: your app calls it, your code checks who the user is, and this SDK returns a token for that user.

It has the same resources, methods and wire fields as the TypeScript SDK. Parameters are plain arrays with the same keys as the TypeScript parameter types, and responses are typed, readonly objects.

You need: PHP 8.1 or later with ext-curl and ext-json, a project, and an API key. See API credentials.

Install

Terminal
composer require livqeno/livqeno-php

The package isn't on Packagist yet. Until it is, install it from a checkout of the repository with a path repository:

composer.json
{
    "repositories": [{ "type": "path", "url": "../Livqeno/sdks/php" }],
    "require": { "livqeno/livqeno-php": "*" }
}

It needs nothing beyond ext-curl and ext-json: no Guzzle, no PSR-18 adapter.

Set up the client

On your backend, create one client and reuse it for every request (for example, bind it as a singleton in your container).

<?php
require 'vendor/autoload.php';
 
$client = new \Livqeno\Client(
    apiKey: getenv('LIVQENO_API_KEY'),
    baseUrl: getenv('LIVQENO_API_URL'), // https://api.livqeno.com
);

Pass the API key yourself. The SDK never reads it from the environment on its own. A missing apiKey (including getenv() returning false) throws RAVEN_INVALID_CONFIG.

ArgumentDefaultNotes
apiKeyrequiredSent as Authorization: Bearer <apiKey>.
baseUrlhttp://localhost:4100Required when APP_ENV is production or prod; leaving it out then throws RAVEN_INVALID_CONFIG.
timeout10.0Seconds per attempt.
maxRetries2Network errors and 429/502/503/504 only.
transportext-curlA Livqeno\Http\TransportInterface, for tests or a custom HTTP stack.

Redirects are never followed, so the bearer token only ever goes to baseUrl.

RTC tokens

Call this when a user is about to join a call, once per user, per join. Take identity from your own login session, never from the request body, or any user could join as anyone.

join-room.php
// Every participant calls this. getOrCreate returns the existing room
// to the second caller instead of a 409.
$room = $client->rooms->create(['name' => 'demo-room', 'getOrCreate' => true]);
 
$grant = $client->tokens->create([
    'room' => $room->id,                // the room's id, not its name
    'identity' => $currentUser->id,     // from your session, never the body
    'permissions' => ['join' => true, 'subscribe' => true, 'publish' => true],
    'expiresIn' => 600,                 // seconds, 30 to 21600
]);
 
header('Content-Type: application/json');
echo json_encode($grant); // forward the whole object

json_encode() on any response object gives back the exact API payload: token, endpoint, iceServers, telemetryUrl, expiresAt and the rest. In Laravel, return response()->json($grant); does the same. Return it whole. The iceServers part is what lets calls work on mobile networks.

Moderating a live call

These methods only work on a participant who is connected to the call right now. If they're not connected, you'll get a 404 with the code RAVEN_PARTICIPANT_NOT_CONNECTED. For $roomId, you can pass either the room's id or its name.

$client->rooms->participants->mute('demo-room', 'alice', ['kind' => 'audio', 'muted' => true]); // or 'trackId'
$client->rooms->participants->updatePermissions('demo-room', 'alice', ['publish' => false]);     // returns the result
$client->rooms->participants->remove('demo-room', 'alice');                                      // doesn't revoke their token

participants->list($roomId) and ->get($roomId, $identity) return null when they can't reach the SFU (the server that routes the call's audio and video). That's different from a room that's genuinely empty, which comes back as []. See Rooms & participants for more.

Chat

Your backend creates conversations, decides who is a member, and mints a chat token for each user. The browser never creates conversations or adds members itself.

$conv = $client->chat->createConversation([
    'name' => 'support-room-42',
    'type' => 'CHANNEL',
    'members' => [['userId' => 'alice', 'role' => 'ADMIN'], ['userId' => 'bob']], // a conversation with no members is unusable
    'getOrCreate' => true,
]);
 
$chatToken = $client->chat->createToken([
    'userId' => $currentUser->id,
    'conversations' => [$conv->publicId],
    'scopes' => ['chat:read', 'chat:send'], // can only narrow the user's role
    'expiresIn' => 900,
]);
// Hand $chatToken->token to the browser. Nothing else.

Members, messages and moderation:

$client->chat->addMember('support-room-42', ['userId' => 'carol', 'role' => 'MODERATOR']);
$client->chat->removeMember('support-room-42', 'carol');
 
$client->chat->sendMessage('support-room-42', [
    'senderId' => 'system', 'type' => 'system', 'text' => 'Maintenance in 5 minutes',
    'clientMessageId' => 'maint-1', // idempotency key
]);
 
$page = $client->chat->listMessages('support-room-42', ['limit' => 50]);
$page->data; $page->nextCursor; $page->hasMore;
$client->chat->deleteMessage('msg_abc'); // soft delete

Live streaming

A live stream is one main host, optional co-hosts, and many viewers who only watch. On your backend: create the stream, start it, then give each person the right credential.

$stream = $client->liveStreams->create(['title' => 'Friday Q&A', 'hostIdentity' => 'user-123']);
$client->liveStreams->start($stream->id); // CREATED -> LIVE
 
// Registering a co-host mints full-publish RTC + moderator chat
// credentials in one call. Hand the result to the client SDK unchanged.
$host = $client->liveStreams->addHost($stream->id, ['identity' => 'user-456', 'role' => 'CO_HOST']);
 
// A viewer credential is always subscribe-only.
$viewer = $client->liveStreams->createViewerToken($stream->id, 'user-789');
 
$client->liveStreams->removeHost($stream->id, 'user-456');
$client->liveStreams->end($stream->id); // LIVE -> ENDED, terminal
  • $client->live is the same object as $client->liveStreams.
  • The method you call decides the role: addHost() makes a host, createViewerToken() makes a viewer. Nothing in the request can change that.
  • For a BROADCAST stream, pass ['acceptPlayback' => true] and read $viewer->playback?->llhlsUrl (or ->url) instead of getting a 409. See Viewers.
  • $client->liveStreams->stage, ->restreams and ->ingress cover the stage, restreaming and OBS ingest.

Webhooks

Livqeno signs every delivery with swalvo-signature: t=<unix>,v1=<hmac>. Livqeno\Webhook compares in constant time (hash_equals) and rejects a timestamp more than 300 s from now, which blocks replays. Always verify the raw body.

webhooks.php
use Livqeno\LivqenoException;
use Livqeno\Webhook;
 
$webhook = new Webhook(getenv('LIVQENO_WEBHOOK_SECRET'));
 
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_LIVQENO_SIGNATURE'] ?? null; // Webhook::SIGNATURE_HEADER
$eventId = $_SERVER['HTTP_LIVQENO_EVENT_ID'] ?? null;     // dedupe on it
 
try {
    $event = $webhook->constructEvent($rawBody, $signature); // verified and json-decoded
} catch (LivqenoException $e) {                              // RAVEN_WEBHOOK_SIGNATURE_INVALID
    http_response_code(400);
    exit;
}
 
if ($event['type'] === 'live_stream.started') {
    // ...
}
http_response_code(204);

In Laravel, verify $request->getContent() against $request->header('swalvo-signature') with $webhook->verify(...), which returns a boolean. Webhook::sign($body, $secret, $t) produces a header exactly as the API does, which helps when you test your own receiver. More: Webhooks.

Errors

Every failure is a Livqeno\LivqenoException. Branch on errorCode, and log requestId: support can use it to find the exact request.

use Livqeno\LivqenoException;
 
try {
    $client->rooms->get('2f1b6c3e-8d4a-4b7e-9c21-5a0e7d3f9b10'); // room ids are UUIDs
} catch (LivqenoException $e) {
    $e->errorCode;  // e.g. 'RAVEN_ROOM_NOT_FOUND'
    $e->statusCode; // e.g. 404, or null for a network or config error
    $e->requestId;  // matches the API's x-request-id
    $e->details;    // the decoded error body
}

The API's own code is passed through. If a proxy strips the JSON body, the code is derived from the status (RAVEN_AUTH_ERROR, RAVEN_NOT_FOUND, RAVEN_RATE_LIMITED, RAVEN_INTERNAL_ERROR, ...). The SDK adds RAVEN_TIMEOUT, RAVEN_NETWORK_ERROR and RAVEN_INVALID_CONFIG, and throws VALIDATION_ERROR before sending when a required parameter is missing. All codes: Error Codes.

Retries, timeouts, and lists

SettingBehavior
RetriesNetwork errors and HTTP 429, 502, 503 and 504 only, with exponential backoff (300 ms, then 600 ms). maxRetries defaults to 2. Never on 400/401/403/404/409/422/500.
Timeoutstimeout, default 10 s, per attempt.
ListsChat history uses cursors ($page->nextCursor, $page->hasMore). Every other list returns a plain array of objects.

$model->toArray() returns the raw payload, including any field newer than your SDK version.

Security

The API key isn't a property of any object, so var_dump, print_r, json_encode and (array) casts of the client can't reach it, and serialize($client) throws. Error messages and details are scrubbed of the key. Constructor parameters are marked #[\SensitiveParameter], which hides the key from stack traces on PHP 8.2+. See Authentication.

Next steps

  1. Build a token endpoint and a working call: Quickstart (choose the PHP tab).
  2. See everything a token can allow: Access tokens.
  3. Set up the app side: Next.js, React, React Native, or Flutter.