PHP server SDK

applooma/server-sdk wraps the AppLooma RTC REST API for PHP backends. It runs on your server, holds your API secret, and issues the join tokens your client apps need.

Install#

Terminal
composer require applooma/server-sdk

Requires PHP 8.0 or newer with the curl and json extensions, both of which are enabled by default in most distributions. The package has no Composer dependencies and autoloads the AppLooma\\ namespace over PSR-4.

Configure credentials#

Read your key pair from the environment. Never write the secret into source control.

.env
APPLOOMA_API_KEY=ak_your_key
APPLOOMA_API_SECRET=as_your_secret
applooma.php
<?php
require __DIR__ . '/vendor/autoload.php';
$apiKey = getenv('APPLOOMA_API_KEY');
$apiSecret = getenv('APPLOOMA_API_SECRET');
if ($apiKey === false || $apiSecret === false) {
throw new RuntimeException('AppLooma credentials are missing');
}
$applooma = new \AppLooma\AppServerClient($apiKey, $apiSecret);

Requests go to https://api.applooma.dev/v1 with the headers x-applooma-key and x-applooma-secret. Pass a third constructor argument for a different region or a self-hosted deployment. An empty key or secret throws \\InvalidArgumentException immediately.

Custom base URL
$applooma = new \AppLooma\AppServerClient($apiKey, $apiSecret, 'https://api.eu.applooma.dev/v1');

A token endpoint#

This is the only endpoint most integrations need. Authenticate your own user first, then decide the identity and role you are willing to grant.

rtc-token.php
<?php
require __DIR__ . '/vendor/autoload.php';
use AppLooma\AppServerClient;
use AppLooma\AppApiException;
header('Content-Type: application/json');
$applooma = new AppServerClient(
getenv('APPLOOMA_API_KEY'),
getenv('APPLOOMA_API_SECRET')
);
// 1. Authenticate YOUR user. This endpoint is the gate to your quota.
$user = authenticate();
if (!$user) {
http_response_code(401);
exit(json_encode(['error' => 'Sign in first']));
}
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$channel = (string) ($body['channel'] ?? '');
// 2. Decide the role on the server. Never trust a role from the client.
$role = isHost($user->id, $channel) ? 'host' : 'audience';
try {
$res = $applooma->createToken([
'roomName' => $channel, // [a-zA-Z0-9_-], 1-64 characters
'identity' => $user->id, // unique per participant
'role' => $role, // host | cohost | audience
'metadata' => ['name' => $user->displayName],
]);
} catch (AppApiException $e) {
error_log("AppLooma {$e->status} {$e->errorCode}: {$e->getMessage()}");
if ($e->errorCode === 'QUOTA_EXCEEDED') {
http_response_code(503);
exit(json_encode(['error' => 'Calling is temporarily unavailable']));
}
http_response_code(502);
exit(json_encode(['error' => 'Could not start the call']));
}
echo json_encode(['token' => $res['token'], 'wsUrl' => $res['wsUrl']]);

In Laravel

Bind the client once as a singleton, then inject it into the controller.

app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->singleton(\AppLooma\AppServerClient::class, function () {
return new \AppLooma\AppServerClient(
config('services.applooma.key'),
config('services.applooma.secret'),
);
});
}
// config/services.php
// 'applooma' => [
// 'key' => env('APPLOOMA_API_KEY'),
// 'secret' => env('APPLOOMA_API_SECRET'),
// ],

Run php artisan config:cache after changing the environment variables so the cached configuration picks them up.

Two participants must never share an identity

Identity is the primary key for a participant. If a second connection joins with an identity that is already in the channel, it replaces the first one.

Method reference#

Every method is synchronous, returns an associative array, and throws \\AppLooma\\AppApiExceptionwhen the API answers with a status of 400 or above. Array keys are the API's own camelCase names — the SDK does not rename them. The HTTP timeout is 15 seconds.

createToken

POST /v1/token — issues a join token for one participant.

public function createToken(array $params): array

Note that createToken takes a single associative array, not positional arguments.

KeyTypeRequiredNotes
roomNamestringYesMust match [a-zA-Z0-9_-] and be 1-64 characters. A 400 is returned otherwise.
identitystringYes1-128 characters. Unique per participant in the channel.
rolestringNoOne of 'host', 'cohost', 'audience'. Defaults to 'host'. host can publish and administer the room, cohost can publish, audience can only subscribe.
metadataarrayNoUnder 2048 bytes when encoded, and must encode to a JSON object rather than an array. Embedded in the token and visible to every participant.

Returns:

KeyTypeDescription
tokenstringThe join token. Valid for 6 hours.
wsUrlstringThe realtime endpoint your client SDK connects to.
roomNamestringThe channel name you asked for, echoed back.
expiresAtstringISO 8601 timestamp of token expiry, 6 hours after issue.

Empty PHP arrays encode as JSON arrays

json_encode([]) produces [], and the API rejects a metadata value that is not an object. Omit the metadata key entirely rather than passing an empty array, or cast it with (object) $metadata.

listRooms

GET /v1/rooms — lists the channels of your project that currently have participants.

public function listRooms(): array

Takes no arguments. Returns a list of associative arrays; the method unwraps the rooms field and returns [] if it is absent.

KeyTypeDescription
roomNamestringThe channel name as you created it.
participantCountintPeople currently connected.
maxParticipantsintCapacity of the channel. 0 means the project default applies.
createdAtstringISO 8601 timestamp of when the channel opened.

listParticipants

GET /v1/rooms/{roomName}/participants — lists who is in one channel right now.

public function listParticipants(string $roomName): array
KeyTypeDescription
identitystringThe identity you issued the token with.
namestringDisplay name, if metadata['name'] was set.
joinedAtstringISO 8601 timestamp of when the participant connected.

The room name is URL-encoded for you. An invalid name returns 400, and a channel that has already closed returns 500.

Example
foreach ($applooma->listRooms() as $room) {
echo $room['roomName'], ' ', $room['participantCount'], PHP_EOL;
foreach ($applooma->listParticipants($room['roomName']) as $p) {
echo ' ', $p['identity'], ' ', $p['joinedAt'], PHP_EOL;
}
}

Error handling#

Failed requests throw \\AppLooma\\AppApiException, a subclass of \\Exception.

AppApiException
namespace AppLooma;
class AppApiException extends \Exception
{
public int $status; // HTTP status, or 0 for a transport failure
public ?string $errorCode; // machine-readable code, when present
// getMessage() gives the human-readable message from the API
}

Use $errorCode, not $code

PHP's built-in \\Exception already declares an integer $codeproperty that cannot be redeclared as a string. The API's machine-readable code is therefore exposed as $e->errorCode. Reading $e->getCode() returns 0 and tells you nothing.

Branching on the code
use AppLooma\AppApiException;
try {
$res = $applooma->createToken([
'roomName' => 'lobby',
'identity' => $user->id,
]);
} catch (AppApiException $e) {
if ($e->status === 0) {
// No HTTP response at all: DNS, TLS or connection failure.
error_log('AppLooma unreachable: ' . $e->getMessage());
throw new RuntimeException('Calling is temporarily unavailable');
}
switch ($e->errorCode) {
case 'QUOTA_EXCEEDED':
// Out of minutes for the month. Alert your team and upgrade.
throw new RuntimeException('Calling is temporarily unavailable');
case 'ACCOUNT_SUSPENDED':
// Billing or policy issue. Only the account owner can resolve it.
throw new RuntimeException('Calling is temporarily unavailable');
default:
if ($e->status === 400) {
throw new InvalidArgumentException($e->getMessage());
}
if ($e->status === 401) {
throw new RuntimeException('AppLooma credentials are wrong');
}
throw $e;
}
}
StatuserrorCodeMeaningWhat to do
0nullThe request never reached us: DNS, TLS or connection failure reported by curl.Check outbound network access from the server. Retry with backoff.
400nullInvalid roomName, identity, role or metadata.Fix the request. Retrying the same body will not help.
401nullMissing, wrong, or deactivated API key or secret.Check your environment variables. If the key was rotated, deploy the new pair.
403ACCOUNT_SUSPENDEDThe account that owns the project is suspended.Resolve the billing or policy issue in the console. No retry will succeed.
404nullThe resource does not exist, or does not belong to your project.Check the identifier you sent.
429QUOTA_EXCEEDEDThe monthly minute allowance is spent.Upgrade the plan. Existing calls continue; new tokens are refused.
429nullRate limited. 600 token requests and 1200 other requests per minute, per project.Back off and retry with jitter. Read the standard RateLimit headers.
500nullServer-side failure.Retry once with backoff. If it persists, contact support with your App ID and the time.

Not every error carries a code

Only QUOTA_EXCEEDED and ACCOUNT_SUSPENDED are emitted today. For everything else $e->errorCode is null, so branch on $e->status as well.

What is not wrapped#

The PHP SDK covers tokens and rooms only. Virtual gifting is wrapped in the Node SDK but not in PHP, and live streaming ingest, RTMP push and cloud recording are not wrapped in any server SDK yet. Call those endpoints directly with the same two headers.

Gifting and streaming with curl
<?php
function applooma_request(string $method, string $path, ?array $body = null): array
{
$ch = curl_init('https://api.applooma.dev/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-applooma-key: ' . getenv('APPLOOMA_API_KEY'),
'x-applooma-secret: ' . getenv('APPLOOMA_API_SECRET'),
],
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
if ($raw === false) {
$err = curl_error($ch);
curl_close($ch);
throw new RuntimeException('AppLooma request failed: ' . $err);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode($raw, true) ?? [];
if ($status >= 400) {
throw new RuntimeException(
sprintf('%d %s: %s', $status, $data['code'] ?? '', $data['error'] ?? 'unknown')
);
}
return $data;
}
// Gift catalogue
$gifts = applooma_request('GET', '/gifts')['gifts'];
// Send a gift. Debit the sender in YOUR coin system first.
$tx = applooma_request('POST', '/gifts/send', [
'roomName' => 'lobby',
'giftId' => $gifts[0]['id'],
'sender' => 'user-42',
'receiver' => 'host-7',
'quantity' => 1,
]);
// $tx = ['txId' => ..., 'coinTotal' => ..., 'sentAt' => ...]
// RTMP ingest for OBS
$ingest = applooma_request('POST', '/streams/ingest', [
'roomName' => 'lobby',
'name' => 'obs-ingest',
'type' => 'rtmp',
]);
// $ingest = ['ingestId' => ..., 'url' => ..., 'streamKey' => ..., 'status' => 'ready']

AppLooma RTC does not move money

Debit the sender in your own coin system before calling /gifts/send. We record the ledger entry, broadcast the animation event to the channel, and fire a gift.sent webhook. Crediting the receiver is your side of the transaction.

Security#

  • The API secret lives on your server only. It never belongs in a browser bundle, a mobile binary, a mobile app config file, or a repository.
  • Keep the secret out of the web root. Load it from the environment or a file above public/.
  • Anyone holding the secret can issue unlimited tokens against your project and spend your quota.
  • Your token endpoint must authenticate the caller. An open token endpoint is the same as publishing your secret.
  • Decide the role on the server. Do not copy role straight from the request body, or any audience member can promote themselves to host.
  • If a secret leaks, rotate the key pair in the console immediately. Deploy the new pair first, then deactivate the old one.
  • Do not log the secret or full tokens, and make sure a stack trace rendered by display_errors never reaches a browser in production.

Next#