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#
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.
APPLOOMA_API_KEY=ak_your_keyAPPLOOMA_API_SECRET=as_your_secret
<?phprequire __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.
$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.
<?phprequire __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.
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.
| Key | Type | Required | Notes |
|---|---|---|---|
roomName | string | Yes | Must match [a-zA-Z0-9_-] and be 1-64 characters. A 400 is returned otherwise. |
identity | string | Yes | 1-128 characters. Unique per participant in the channel. |
role | string | No | One of 'host', 'cohost', 'audience'. Defaults to 'host'. host can publish and administer the room, cohost can publish, audience can only subscribe. |
metadata | array | No | Under 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:
| Key | Type | Description |
|---|---|---|
token | string | The join token. Valid for 6 hours. |
wsUrl | string | The realtime endpoint your client SDK connects to. |
roomName | string | The channel name you asked for, echoed back. |
expiresAt | string | ISO 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.
| Key | Type | Description |
|---|---|---|
roomName | string | The channel name as you created it. |
participantCount | int | People currently connected. |
maxParticipants | int | Capacity of the channel. 0 means the project default applies. |
createdAt | string | ISO 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
| Key | Type | Description |
|---|---|---|
identity | string | The identity you issued the token with. |
name | string | Display name, if metadata['name'] was set. |
joinedAt | string | ISO 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.
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.
namespace AppLooma;class AppApiException extends \Exception{public int $status; // HTTP status, or 0 for a transport failurepublic ?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.
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;}}
| Status | errorCode | Meaning | What to do |
|---|---|---|---|
| 0 | null | The request never reached us: DNS, TLS or connection failure reported by curl. | Check outbound network access from the server. Retry with backoff. |
| 400 | null | Invalid roomName, identity, role or metadata. | Fix the request. Retrying the same body will not help. |
| 401 | null | Missing, wrong, or deactivated API key or secret. | Check your environment variables. If the key was rotated, deploy the new pair. |
| 403 | ACCOUNT_SUSPENDED | The account that owns the project is suspended. | Resolve the billing or policy issue in the console. No retry will succeed. |
| 404 | null | The resource does not exist, or does not belong to your project. | Check the identifier you sent. |
| 429 | QUOTA_EXCEEDED | The monthly minute allowance is spent. | Upgrade the plan. Existing calls continue; new tokens are refused. |
| 429 | null | Rate limited. 600 token requests and 1200 other requests per minute, per project. | Back off and retry with jitter. Read the standard RateLimit headers. |
| 500 | null | Server-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.
<?phpfunction 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
rolestraight 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_errorsnever reaches a browser in production.