Python server SDK

The applooma package wraps the AppLooma RTC REST API for Python backends. It runs on your server, holds your API secret, and issues the join tokens your client apps need.

Install#

Terminal
pip install applooma

Requires Python 3.9 or newer. The package builds on urllib from the standard library and pulls in no third-party 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_client.py
import os
from applooma import AppServerClient
applooma = AppServerClient(
api_key=os.environ["APPLOOMA_API_KEY"],
api_secret=os.environ["APPLOOMA_API_SECRET"],
)

Using os.environ[...] rather than os.getenv means a missing variable raises at import time instead of producing a confusing failure on the first user request.

Requests go to https://api.applooma.dev/v1 with the headers x-applooma-key and x-applooma-secret. Pass base_url= to the constructor for a different region or a self-hosted deployment. An empty key or secret raises ValueError immediately.

The client is synchronous

Calls block for up to 15 seconds. In an async framework, run them in a worker thread — the FastAPI example below shows how.

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.

main.py
import os
from fastapi import FastAPI, Depends, HTTPException
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel
from applooma import AppServerClient, AppApiError
app = FastAPI()
applooma = AppServerClient(
api_key=os.environ["APPLOOMA_API_KEY"],
api_secret=os.environ["APPLOOMA_API_SECRET"],
)
class TokenRequest(BaseModel):
channel: str
@app.post("/api/rtc-token")
async def rtc_token(body: TokenRequest, user=Depends(current_user)):
# Decide the role on the server. Never trust a role from the client.
role = "host" if await is_host(user.id, body.channel) else "audience"
try:
# The SDK is synchronous, so keep it off the event loop.
result = await run_in_threadpool(
applooma.create_token,
room_name=body.channel, # [a-zA-Z0-9_-], 1-64 characters
identity=user.id, # unique per participant
role=role,
metadata={"name": user.display_name},
)
except AppApiError as err:
if err.code == "QUOTA_EXCEEDED":
raise HTTPException(503, "Calling is temporarily unavailable")
if err.status == 400:
raise HTTPException(400, err.args[0])
raise HTTPException(502, "Could not start the call")
return {"token": result["token"], "wsUrl": result["wsUrl"]}

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 and raises AppApiErrorwhen the API answers with a status of 400 or above. Responses are returned as plain dictionaries with the API's own camelCase keys — the SDK does not rename them.

create_token

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

create_token(room_name: str, identity: str, role: str = "host", metadata: dict | None = None) -> dict
ParameterTypeRequiredNotes
room_namestrYesMust match [a-zA-Z0-9_-] and be 1-64 characters. A 400 is returned otherwise.
identitystrYes1-128 characters. Unique per participant in the channel.
rolestrNoOne of "host", "cohost", "audience". Defaults to "host". host can publish and administer the room, cohost can publish, audience can only subscribe.
metadatadict | NoneNoUnder 2048 bytes when serialised. Embedded in the token and visible to every participant, so keep secrets out of it. Omitted from the request when None.

Returns a dict:

KeyTypeDescription
tokenstrThe join token. Valid for 6 hours.
wsUrlstrThe realtime endpoint your client SDK connects to.
roomNamestrThe channel name you asked for, echoed back.
expiresAtstrISO 8601 timestamp of token expiry, 6 hours after issue.

metadata['name'] becomes the display name

If metadata contains a name key 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.

list_rooms

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

list_rooms() -> list

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

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

list_participants

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

list_participants(room_name: str) -> list
KeyTypeDescription
identitystrThe identity you issued the token with.
namestrDisplay name, if metadata["name"] was set.
joinedAtstrISO 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
for room in applooma.list_rooms():
print(room["roomName"], room["participantCount"])
for p in applooma.list_participants(room["roomName"]):
print(" ", p["identity"], p.get("name"), p["joinedAt"])

Error handling#

Failed requests raise AppApiError, a subclass of Exception. It carries the HTTP status and, where the API supplies one, a machine-readable code.

AppApiError
class AppApiError(Exception):
status: int # HTTP status
code: str | None # machine-readable code, when present
# str(err) gives the human-readable message from the API
Branching on the code
from applooma import AppApiError
try:
result = applooma.create_token(room_name="lobby", identity=user.id)
except AppApiError as err:
if err.code == "QUOTA_EXCEEDED":
# Out of minutes for the month. Alert your team and upgrade the plan.
alert_ops("AppLooma quota exhausted")
raise ServiceUnavailable()
if err.code == "ACCOUNT_SUSPENDED":
# Billing or policy issue. Only the account owner can resolve it.
raise ServiceUnavailable()
if err.status == 400:
raise BadRequest(str(err))
if err.status == 401:
raise RuntimeError("AppLooma credentials are wrong")
raise
StatuscodeMeaningWhat to do
400NoneInvalid room_name, identity, role or metadata.Fix the request. Retrying the same body will not help.
401NoneMissing, 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.
404NoneThe 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.
429NoneRate limited. 600 token requests and 1200 other requests per minute, per project.Back off and retry with jitter. Read the standard RateLimit headers.
500NoneServer-side failure.Retry once with backoff. If it persists, contact support with your App ID and the time.

Network failures are not AppApiError

A DNS failure, a refused connection or a timeout raises urllib.error.URLError (or socket.timeout) rather than AppApiError, because no HTTP response arrived. Catch both if you want a single failure path.

Not every error carries a code

Only QUOTA_EXCEEDED and ACCOUNT_SUSPENDED are emitted today. For everything else err.code is None, so branch on err.status as well.

What is not wrapped#

The Python SDK covers tokens and rooms only. Virtual gifting is wrapped in the Node SDK but not in Python, 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 requests
import os
import requests
BASE = "https://api.applooma.dev/v1"
HEADERS = {
"Content-Type": "application/json",
"x-applooma-key": os.environ["APPLOOMA_API_KEY"],
"x-applooma-secret": os.environ["APPLOOMA_API_SECRET"],
}
def gift_catalog() -> list:
res = requests.get(f"{BASE}/gifts", headers=HEADERS, timeout=15)
res.raise_for_status()
return res.json()["gifts"]
def send_gift(room_name: str, gift_id: str, sender: str, receiver: str, quantity: int = 1) -> dict:
# Debit the sender in YOUR coin system before calling this.
res = requests.post(
f"{BASE}/gifts/send",
headers=HEADERS,
json={
"roomName": room_name,
"giftId": gift_id,
"sender": sender,
"receiver": receiver,
"quantity": quantity,
},
timeout=15,
)
if res.status_code >= 400:
body = res.json()
raise RuntimeError(f"{res.status_code} {body.get('code')}: {body.get('error')}")
return res.json() # {"txId", "coinTotal", "sentAt"}
def create_ingest(room_name: str) -> dict:
res = requests.post(
f"{BASE}/streams/ingest",
headers=HEADERS,
json={"roomName": room_name, "name": "obs-ingest", "type": "rtmp"},
timeout=15,
)
res.raise_for_status()
return res.json() # {"ingestId", "url", "streamKey", "roomName", "status"}

If you prefer no third-party dependency, the same calls work with urllib.request; requests is used here only for brevity.

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. Deploy the new pair first, then deactivate the old one.
  • Do not log the secret or full tokens. Log err.status and err.code instead.

Next#