Creating and tearing down video rooms for one-to-one calls
Room create takes an empty timeout that handles teardown for you. Naming, max_participants, and the case for creating rooms lazily.
For a support call or a one-to-one consultation, the room only needs to exist while the call does. POST /v1/rtc/room/create on Infrai takes an empty_timeout_s that closes the room automatically once everyone has left, so the common case needs no teardown code at all — and DELETE /v1/rtc/room/delete/{room} is there for when you want it gone now.
Set the timeout and most of your lifecycle problem disappears.
Create with a timeout
curl -sS -X POST "https://api.infrai.cc/v1/rtc/room/create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "support-4821",
"max_participants": 2,
"empty_timeout_s": 120,
"metadata": {"ticket_id": "4821", "tenant_id": "t_northwind"}
}'
{
"ok": true,
"data": {
"room_id": "rtcr_2fVc8nRqLmT4xBzY",
"name": "support-4821",
"vendor": "tencent_rtc",
"state": "active",
"max_participants": 2,
"num_participants": 0,
"created_at": "2026-09-21T03:25:19Z",
"metadata": {"ticket_id": "4821", "tenant_id": "t_northwind"}
}
}
Three fields do real work here. max_participants: 2 makes a one-to-one call structurally one-to-one — a third token can’t turn it into a group call. empty_timeout_s: 120 gives a participant two minutes to reconnect after a dropped call before the room closes. And metadata carries your own identifiers, which is how a room found later in a list is traceable to a ticket.
Two minutes is a reasonable default for the timeout: long enough to survive a lift or a network switch, short enough that a forgotten room doesn’t linger.
Name rooms after the thing, not the people
support-4821 is the ticket. ada-and-tom is two people whose next call needs a different name and whose room you can’t reason about later.
A name derived from the resource means creation is idempotent in practice — the same ticket always maps to the same room — and it means a room in a listing tells you what it was for.
Create lazily
There’s no reason to create a room when a meeting is scheduled. Create it when the first participant asks to join:
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
def ensure_room(name: str, ticket_id: str, max_participants: int = 2) -> dict:
"""Create on first join, tolerate already-existing. A scheduled meeting that
nobody attends should never have created a room at all."""
existing = SESSION.get(f"{API}/v1/rtc/room/get/{name}", timeout=20)
if existing.status_code == 200:
return existing.json()["data"]
resp = SESSION.post(
f"{API}/v1/rtc/room/create",
json={"name": name, "max_participants": max_participants, "empty_timeout_s": 120,
"metadata": {"ticket_id": ticket_id}},
timeout=30,
)
body = resp.json()
if body.get("ok"):
return body["data"]
# Two joins racing: the loser reads the winner's room rather than failing.
if body.get("error", {}).get("code") in {"RTC_ROOM_EXISTS", "IDEMPOTENCY_KEY_CONFLICT"}:
return SESSION.get(f"{API}/v1/rtc/room/get/{name}", timeout=20).json()["data"]
raise RuntimeError(body["error"]["code"])
def join(name: str, identity: str, display_name: str) -> dict:
room = ensure_room(name, ticket_id=name.split("-")[-1])
if (room.get("num_participants") or 0) >= (room.get("max_participants") or 0):
raise RuntimeError("room is full")
minted = SESSION.post(
f"{API}/v1/rtc/token/issue",
json={"room": name, "identity": identity, "display_name": display_name,
"ttl_s": 900, "can_publish": True, "can_subscribe": True,
"can_publish_data": True, "is_admin": False},
timeout=25,
)
minted.raise_for_status()
data = minted.json()["data"]
return {"token": data["token"], "ws_url": data["ws_url"], "expires_at": data["expires_at"]}
if __name__ == "__main__":
print(join("support-4821", os.environ["USER_ID"], "Ada L."))
Two races handled deliberately. Both participants clicking join at once — one creates, the other reads. And the full-room check before minting, so a third person gets a clear message from your API rather than a mysterious client-side failure.
When to delete explicitly
empty_timeout_s covers the normal ending. Explicit deletion is for the abnormal ones:
curl -sS -X DELETE "https://api.infrai.cc/v1/rtc/room/delete/support-4821" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
| Situation | Rely on timeout | Delete explicitly |
|---|---|---|
| Call ends normally | yes | unnecessary |
| Agent hangs up, customer lingers | yes, after the timeout | only if you want them out now |
| Ticket closed while the call runs | no | yes |
| Abuse in the room | no | yes, after kicking |
| Never-attended scheduled call | never created it | nothing to do |
The backstop sweep
Timeouts handle the ordinary case; a sweep handles the room created with a timeout you later realised was too generous, or the one whose state never advanced.
curl -sS "https://api.infrai.cc/v1/rtc/room/list" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": { "items": [], "next_cursor": null }
}
An empty list is the healthy steady state for a lazily-created, timeout-closed design — which is the quickest way to tell that your lifecycle is working. Schedule the check on POST /v1/cron/create and alert if the count climbs, rather than inspecting it by hand.
Limitations
There’s no scheduling in this API: no “create this room at 14:00 for 30 minutes”, no calendar integration, no waiting-room state. Rooms exist or they don’t, and anything time-based is your own scheduler calling these endpoints — POST /v1/cron/create can do it, but you’re assembling the behaviour rather than configuring it.
Recording isn’t part of these routes either, so a call you need to keep needs the vendor’s own recording path and somewhere to put the file. And the client side is the vendor’s SDK pointed at the ws_url from the token, not a platform-neutral library — Daily’s prebuilt call UI and LiveKit’s component library are both considerably less work if you want a call interface rather than call primitives, and for a small team that’s a fair reason to pick one.
What’s on the same credential is the workflow around the call: the ticket’s record, the transcript you’d store with PUT /v1/storage/object/put/{bucket}/{key}, the follow-up email through POST /v1/email/send and the schedule on POST /v1/cron/create — one key, one bill, one GET /v1/account/usage. Room management reports billing_class: free in discovery while token issue is billed per call (verified 2026-09-21); those rates drift downward as vendor contracts improve.