Node.js server SDK

@applooma/server-sdk is a thin, dependency-free wrapper over the AppLooma RTC REST API. It runs on your server, holds your API secret, and issues the join tokens your client apps need.

Install#

Terminal
npm install @applooma/server-sdk

The package uses the global fetch function, so it requires Node.js 18 or newer. It ships CommonJS and ESM builds plus TypeScript types, and has no runtime dependencies.

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.js
import { AppServerClient } from '@applooma/server-sdk';
export const applooma = new AppServerClient({
apiKey: process.env.APPLOOMA_API_KEY,
apiSecret: process.env.APPLOOMA_API_SECRET,
});

Every request is sent to https://api.applooma.dev/v1 with the headers x-applooma-key and x-applooma-secret. Pass baseUrl to the constructor to point at a different region or a self-hosted deployment.

The constructor throws a plain Error if apiKey or apiSecret is empty, so a misconfigured deployment fails at startup rather than on the first user request.

A token endpoint in Express#

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

server.js
import express from 'express';
import { AppServerClient, AppApiError } from '@applooma/server-sdk';
const app = express();
app.use(express.json());
const applooma = new AppServerClient({
apiKey: process.env.APPLOOMA_API_KEY,
apiSecret: process.env.APPLOOMA_API_SECRET,
});
app.post('/api/rtc-token', async (req, res) => {
// 1. Authenticate YOUR user. This endpoint is the gate to your quota.
const user = await authenticate(req);
if (!user) return res.status(401).json({ error: 'Sign in first' });
// 2. Decide the channel and role yourself. Do not take the role
// from the request body unless you have checked it.
const channel = String(req.body.channel || '');
const role = user.id === (await hostOf(channel)) ? 'host' : 'audience';
try {
const result = await applooma.createToken({
roomName: channel, // [a-zA-Z0-9_-], 1-64 characters
identity: user.id, // unique per participant
role, // 'host' | 'cohost' | 'audience'
metadata: { name: user.displayName },
});
res.json({ token: result.token, wsUrl: result.wsUrl });
} catch (err) {
if (err instanceof AppApiError) {
console.error('AppLooma', err.status, err.code, err.message);
if (err.code === 'QUOTA_EXCEEDED') {
return res.status(503).json({ error: 'Calling is temporarily unavailable' });
}
return res.status(502).json({ error: 'Could not start the call' });
}
throw err;
}
});
app.listen(3001);

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#

All methods are asynchronous and return a promise. Every one of them rejects with an AppApiError when the API answers with a status of 400 or above.

createToken

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

createToken(params: CreateTokenParams): Promise<TokenResponse>
ParameterTypeRequiredNotes
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.
role'host' | 'cohost' | 'audience'NoDefaults to 'host'. host can publish and administer the room, cohost can publish, audience can only subscribe.
metadataRecord<string, unknown>NoA plain object under 2048 bytes when serialised. It is embedded in the token and visible to every participant, so do not put secrets in it.

Resolves to:

FieldTypeDescription
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.

metadata.name becomes the display name

If metadata contains a name field it is also used as the participant display name. The service overwrites appId and role inside metadata with its own values, so a caller cannot claim a role it was not granted.

listRooms

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

listRooms(): Promise<AppRoom[]>

Takes no arguments. Resolves to an array; the method unwraps the rooms field of the response for you.

FieldTypeDescription
roomNamestringThe channel name as you created it.
participantCountnumberPeople currently connected.
maxParticipantsnumberCapacity 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.

listParticipants(roomName: string): Promise<{ identity: string; name?: string; joinedAt: string | number }[]>
FieldTypeDescription
identitystringThe identity you issued the token with.
namestring | undefinedDisplay 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.

getGifts

GET /v1/gifts — the active gift catalogue for your project, as configured in the console.

getGifts(): Promise<AppGift[]>
FieldTypeDescription
idstringGift identifier. Pass this to sendGift.
namestringDisplay name.
imageUrlstringHTTPS URL of the artwork.
coinPricenumberPrice in your own coin unit.

sendGift

POST /v1/gifts/send — records a gift and broadcasts an applooma.gift data message to everyone in the channel.

sendGift(params: SendGiftParams): Promise<{ txId: string; coinTotal: number; sentAt: string }>
ParameterTypeRequiredNotes
roomNamestringYesMust match [a-zA-Z0-9_-]{1,64}.
giftIdstringYesAn active gift in your catalogue. 404 if not found.
senderstringYesIdentity of the sender.
receiverstringYesIdentity of the receiver.
quantitynumberNoInteger 1-9999. Defaults to 1.

AppLooma RTC does not move money

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

The broadcast is best effort. If the channel closed between the debit and the call, the ledger entry is still written and the call still succeeds.

giftLeaderboard

GET /v1/gifts/leaderboard — top senders and receivers, project-wide or for one channel.

giftLeaderboard(roomName?: string): Promise<{
topSenders: { identity: string; coins: number; gifts: number }[];
topReceivers: { identity: string; coins: number; gifts: number }[];
}>

Omit roomName for a project-wide leaderboard. Each list holds at most 20 entries, ordered by coins descending.

Error handling#

Failed requests reject with AppApiError. It carries the HTTP status and, where the API supplies one, a machine-readable code.

AppApiError
class AppApiError extends Error {
name: 'AppApiError';
status: number; // HTTP status
message: string; // human-readable message from the API
code?: string; // machine-readable code, when present
}
Branching on the code
import { AppApiError } from '@applooma/server-sdk';
try {
await applooma.createToken({ roomName: 'lobby', identity: user.id });
} catch (err) {
if (!(err instanceof AppApiError)) throw err;
switch (err.code) {
case 'QUOTA_EXCEEDED':
// Out of minutes for the month. Alert your team and upgrade the plan.
return res.status(503).json({ error: 'Calling is temporarily unavailable' });
case 'ACCOUNT_SUSPENDED':
// Billing or policy issue. Only the account owner can resolve it.
return res.status(503).json({ error: 'Calling is temporarily unavailable' });
default:
if (err.status === 400) return res.status(400).json({ error: err.message });
if (err.status === 401) throw new Error('AppLooma credentials are wrong');
return res.status(502).json({ error: 'Upstream error' });
}
}
StatuscodeMeaningWhat to do
400—Invalid roomName, identity, role or metadata.Fix the request. Retrying the same body will not help.
401—Missing, 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.
404—Gift not found, or not active in your catalogue.Check the gift id against getGifts().
429QUOTA_EXCEEDEDThe monthly minute allowance is spent.Upgrade the plan. Existing calls continue; new tokens are refused.
429—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—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 err.code is undefined, so branch on err.status as well.

What is not wrapped#

The Node SDK covers tokens, rooms and gifts. Live streaming ingest, RTMP push and cloud recording are not wrapped yet. Call them with fetch until they are.

Calling an unwrapped endpoint
const BASE = 'https://api.applooma.dev/v1';
const headers = {
'content-type': 'application/json',
'x-applooma-key': process.env.APPLOOMA_API_KEY,
'x-applooma-secret': process.env.APPLOOMA_API_SECRET,
};
// Create an RTMP ingest for OBS
const res = await fetch(`${BASE}/streams/ingest`, {
method: 'POST',
headers,
body: JSON.stringify({ roomName: 'lobby', name: 'obs-ingest', type: 'rtmp' }),
});
if (!res.ok) throw new Error(`ingest failed: ${res.status}`);
const { ingestId, url, streamKey } = await res.json();
// Give url + streamKey to the broadcaster to paste into OBS.

The Python and PHP SDKs do not wrap gifts. If you are writing a polyglot backend, keep the gifting calls in the Node service or call the REST endpoints directly.

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.
  • 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. The old secret stops working as soon as you deactivate it, so deploy the new pair first, then deactivate the old.
  • Do not log the secret or full tokens. Log the status and code from AppApiError instead.

Next#