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#
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.
APPLOOMA_API_KEY=ak_your_keyAPPLOOMA_API_SECRET=as_your_secret
import osfrom applooma import AppServerClientapplooma = 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.
import osfrom fastapi import FastAPI, Depends, HTTPExceptionfrom fastapi.concurrency import run_in_threadpoolfrom pydantic import BaseModelfrom applooma import AppServerClient, AppApiErrorapp = 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 charactersidentity=user.id, # unique per participantrole=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
| Parameter | Type | Required | Notes |
|---|---|---|---|
room_name | str | Yes | Must match [a-zA-Z0-9_-] and be 1-64 characters. A 400 is returned otherwise. |
identity | str | Yes | 1-128 characters. Unique per participant in the channel. |
role | str | No | One of "host", "cohost", "audience". Defaults to "host". host can publish and administer the room, cohost can publish, audience can only subscribe. |
metadata | dict | None | No | Under 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:
| Key | Type | Description |
|---|---|---|
token | str | The join token. Valid for 6 hours. |
wsUrl | str | The realtime endpoint your client SDK connects to. |
roomName | str | The channel name you asked for, echoed back. |
expiresAt | str | ISO 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.
| Key | Type | Description |
|---|---|---|
roomName | str | 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 | str | ISO 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
| Key | Type | Description |
|---|---|---|
identity | str | The identity you issued the token with. |
name | str | Display name, if metadata["name"] was set. |
joinedAt | str | 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.
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.
class AppApiError(Exception):status: int # HTTP statuscode: str | None # machine-readable code, when present# str(err) gives the human-readable message from the API
from applooma import AppApiErrortry: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
| Status | code | Meaning | What to do |
|---|---|---|---|
| 400 | None | Invalid room_name, identity, role or metadata. | Fix the request. Retrying the same body will not help. |
| 401 | None | 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 | None | 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 | None | 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 | None | Server-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.
import osimport requestsBASE = "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
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. Log
err.statusanderr.codeinstead.