Feedback Community Room Platform Developers Docs API Reference AI Agents Create a Room فارسی
REST Control API · v1

API Reference

One small control plane, two surfaces: an open Public API for link-based rooms and a Tenant API for products that own their rooms. JSON in, JSON out. The live machine-readable contract is served by the API itself at /api/v1/openapi.yaml.

Conventions

Base URLs, auth & limits

Base URLs

Production APIhttps://api.videorayan.ir/v1
Same-origin (our portals)https://en.videorayan.ir/api/v1
Media signalingwss://rtc.videorayan.ir
Hosted room UIhttps://en.videorayan.ir/r/{roomId}

Ground rules

  • All bodies are JSON with Content-Type: application/json. Unknown fields are rejected.
  • Tenant routes require Authorization: Bearer <key> (keys look like rtck_…).
  • Public & guest routes take no key — the room link is the credential.
  • Rate limit: 120 requests / minute per tenant key (or client IP on public routes). Exceeding it returns 429.
  • Cross-tenant access returns 404 (not 403) to prevent resource probing.
Calling from a browser on videorayan.ir domains? Use the same-origin path /api/v1/… — one connection, no CORS preflight. Other origins need to be allow-listed by the operator (ALLOWED_ORIGINS).
Public API — no key required

Free rooms: create, resolve, join

Public rooms live in a dedicated internal tenant whose API key is discarded at boot — they are reachable only through these routes. Room names are the handles: creating a room with an existing name returns the same room (handy for standing links like weekly-standup).

POST /v1/public/rooms 201 Created

Create (or fetch) a free browser-created room. Rate-limited per client IP.

Request body

FieldTypeRequiredNotes
namestringyes1–64 chars. The room's public handle. Re-creating an existing name returns the same room.
passwordstringno1–64 chars. When set, guests must present it to join (verified server-side).

Response 201

application/json
{
  "roomId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "name": "weekly-standup"
}

Example

curl -X POST https://api.videorayan.ir/v1/public/rooms \
  -H "Content-Type: application/json" \
  -d '{"name":"weekly-standup","password":"s3cret"}'
const res = await fetch('https://api.videorayan.ir/v1/public/rooms', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'weekly-standup', password: 's3cret' })
});
const { roomId } = await res.json();
import requests

r = requests.post(
    "https://api.videorayan.ir/v1/public/rooms",
    json={"name": "weekly-standup", "password": "s3cret"},
)
roomId = r.json()["roomId"]

Errors

429 rate limited · 400 validation ({"error":"invalid_input"} — name/password lengths, unknown fields)

GET /v1/public/rooms/{ref} 200 OK

Resolve a room by UUID or name before joining. Call this first to learn whether the room needs a password — exactly what the hosted join page does.

Response 200

application/json
{
  "roomId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "name": "weekly-standup",
  "needsPassword": true
}

Errors

404 {"error":"room_not_found"} — unknown or expired ref.

POST /v1/guest/rooms/{ref}/tokens 201 Created

Issue a guest join token. {ref} may be the room UUID or the public room name. The participant cap and room expiry are enforced server-side; every issue is audited and heavily rate-limited.

Request body

FieldTypeRequiredNotes
identitystringyes1–40 chars. Displayed to other participants; must be unique in the room.
passwordstringif room is private1–64 chars. Required when the room was created with a password.

Response 201

application/json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",  // LiveKit JWT, single-room scope
  "identity": "sara",
  "roomName": "weekly-standup",
  "expiresAt": "2026-10-26T12:40:00.000Z"
}

Errors

403 wrong or missing password · 404 room not found · 409 room full or already ended · 429 rate limited

Handle 403 by re-prompting. The hosted flow resolves the room first (needsPassword) so users only see the password field when it is actually required — see the browser-flow guide.
Tenant API — Bearer key

Owned rooms for production products

Every room belongs to a tenant. Create rooms with your own correlation ID, mint host or guest tokens with TTLs up to an hour, inspect live participants, and end rooms server-side. Keys are issued by the operator (rtck_…, shown once at bootstrap) and belong in your backend only.

Authentication — send the key on every tenant call:
header
Authorization: Bearer rtck_live_9f2…   # never ship this to a front-end
POST /v1/rooms 201 Created 200 idempotent replay

Create a tenant-owned room. Send an Idempotency-Key header (any unique string, ≤128 chars) — retrying with the same key returns the original room with 200 instead of creating a duplicate.

Request body

FieldTypeRequiredNotes
externalIdstring ≤128noYour own reference (e.g. appointment-123). Returned in every room response.
maxParticipantsinteger 1–3noDefault 3 (pilot cap; operator-configurable up to 10).
expiresInSecondsinteger 60–86400noDefault 7200 (2 h). After expiry the room is swept and stale tokens stop working.
metadatastring ≤512noOpaque blob for your own bookkeeping.

Response 201

Room
{
  "roomId": "b21b2f50-7a1d-4f6e-9c8f-3d2a1b4c5d6e",
  "tenantId": "t_9a8b7c6d",
  "externalId": "appointment-123",
  "status": "active",
  "maxParticipants": 3,
  "createdAt": "2026-10-26T10:00:00.000Z",
  "expiresAt": "2026-10-26T12:00:00.000Z"
}

Example

curl -X POST https://api.videorayan.ir/v1/rooms \
  -H "Authorization: Bearer rtck_live_9f2…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: appointment-123-2026-10-26" \
  -d '{"externalId":"appointment-123","maxParticipants":3,"expiresInSeconds":3600}'
// server-side only — the key never reaches the browser
const res = await fetch('https://api.videorayan.ir/v1/rooms', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.VIDEORAYAN_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'appointment-123-2026-10-26'
  },
  body: JSON.stringify({ externalId: 'appointment-123', maxParticipants: 3 })
});
const room = await res.json();
import os, requests

r = requests.post(
    "https://api.videorayan.ir/v1/rooms",
    headers={"Authorization": f"Bearer {os.environ['VIDEORAYAN_KEY']}",
             "Idempotency-Key": "appointment-123-2026-10-26"},
    json={"externalId": "appointment-123", "maxParticipants": 3},
)
room = r.json()

Errors

401 missing/invalid key · 429 rate limited

GET /v1/rooms/{roomId} 200 OK

Fetch room state (owner tenant only). Status is one of active, ended, or expired.

Errors

403 not your room · 404 unknown room (cross-tenant also 404)

DELETE /v1/rooms/{roomId} 204 No Content

End the room: it is closed in LiveKit, everyone is disconnected, new tokens are rejected, and re-joining is blocked.

Errors

403 not your room · 404 unknown room

POST /v1/rooms/{roomId}/tokens 201 Created

Mint a join token for one participant. Call this from your backend per user, right before they enter.

Request body

FieldTypeRequiredNotes
identitystring ≤128yesUnique per room; shown to other participants.
namestring ≤128noHuman-friendly display name.
rolehost | guestnoDefault guest.
ttlSecondsinteger 30–3600noDefault 600 (10 min). Re-mint when expired — tokens are never renewable.

Response 201

application/json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "wsUrl": "wss://rtc.videorayan.ir",
  "identity": "user-42",
  "expiresAt": "2026-10-26T10:10:00.000Z"
}

Errors

401 auth · 403 not your room · 404 unknown room · 409 room full or ended

GET /v1/rooms/{roomId}/participants 200 OK

List everyone currently connected (owner tenant only).

application/json
{
  "participants": [
    { "id": "PA_x1", "identity": "dr.ahmadi", "name": "Dr. Ahmadi", "joinedAt": "2026-10-26T10:01:12.000Z" },
    { "id": "PA_x2", "identity": "user-42",  "name": null,               "joinedAt": "2026-10-26T10:02:03.000Z" }
  ]
}

Errors

403 not your room

DELETE /v1/rooms/{roomId}/participants/{participantId} 204 No Content

Disconnect a participant immediately (moderation). Use the id from the participants list.

Errors

403 not your room · 404 unknown room/participant

Ops & Contract

Health, readiness & the live spec

GET /v1/health

Liveness — service is up. No auth, no sensitive info. Point your monitors here.

GET /v1/ready

Readiness — PostgreSQL and LiveKit dependencies checked. 503 when a dependency is down.

GET /v1/openapi.yaml

The live OpenAPI 3.0 contract for the tenant surface — import into Postman/Insomnia or generate a client. A static JSON copy is also at /openapi.json.

GET /v1/whoami

Verify a tenant key with zero side effects — returns the tenant name behind your Authorization: Bearer rtck_… header (401 when invalid).

Verified Live

Executed against production — 2026-10-27

These are not mocked examples. Every call below was run against the live API the day this page shipped; outputs are verbatim (JWTs and tenant names shortened).

public-flow.sh — community room (real output)
# create-or-fetch (idempotent — same name → same room)
$ curl -s -X POST https://api.videorayan.ir/v1/public/rooms \
    -H "Content-Type: application/json" -d '{"name":"community"}'
{"roomId":"4843ba3a-e800-475a-b78c-7ae60c9d33d7","name":"community"}   # 201

$ curl -s https://api.videorayan.ir/v1/public/rooms/community
{"roomId":"4843ba3a-e800-475a-b78c-7ae60c9d33d7","name":"community","needsPassword":false}   # 200

$ curl -s -X POST https://api.videorayan.ir/v1/guest/rooms/community/tokens \
    -H "Content-Type: application/json" -d '{"identity":"SaraDev"}'
{"token":"eyJhbGciOiJIUzI1NiJ9.…","identity":"SaraDev","roomName":"community","expiresAt":"2026-10-27T14:03:34.433Z"}   # 201

# humans join with: https://en.videorayan.ir/r/community
tenant-flow.sh — owned room lifecycle (real output)
# key check — zero side effects
$ curl -s https://api.videorayan.ir/v1/whoami -H "Authorization: Bearer rtck_…"
{"tenant":"acme"}   # 200 — tenant name masked

$ curl -s -X POST https://api.videorayan.ir/v1/rooms \
    -H "Authorization: Bearer rtck_…" -H "Idempotency-Key: doc-demo-20261027" \
    -H "Content-Type: application/json" \
    -d '{"externalId":"doc-demo","maxParticipants":3,"expiresInSeconds":3600}'
{"roomId":"d39c083e-c41e-46bf-812f-765c8590235f","tenantId":"…","externalId":"doc-demo","status":"active","maxParticipants":3,"createdAt":"2026-10-27T13:57:29.688Z","expiresAt":"2026-10-27T14:57:29.641Z"}   # 201

$ curl -s -X POST https://api.videorayan.ir/v1/rooms/d39c083e…/tokens \
    -H "Authorization: Bearer rtck_…" -H "Content-Type: application/json" \
    -d '{"identity":"host-1","name":"Dr. Rezaei","role":"host","ttlSeconds":600}'
{"token":"eyJhbGciOiJIUzI1NiJ9.…","wsUrl":"wss://rtc.videorayan.ir","identity":"host-1","expiresAt":"2026-10-27T14:07:29.974Z"}   # 201

$ curl -s https://api.videorayan.ir/v1/rooms/d39c083e…/participants -H "Authorization: Bearer rtck_…"
{"participants":[]}   # 200

$ curl -s -X DELETE https://api.videorayan.ir/v1/rooms/d39c083e… -H "Authorization: Bearer rtck_…"   # 204

Real error bodies (captured live)

errors.sh
$ curl -s …/public/rooms/no-such-room-xyz        # 404
{"error":"room_not_found"}

$ curl -s -X POST …/public/rooms -d '{"name":""}'  # 400
{"error":"invalid_input"}

$ curl -s -X POST …/guest/rooms/x/tokens -d '{"identity":"eve","password":"wrong"}'   # 403
{"error":"wrong_password"}

Validation failures return 400 with invalid_input (not 422). Branch on the JSON error field, not just the status.

Re-verify in one line

Run this whenever you want proof the platform is up and the contract is live:

healthcheck.sh
curl -fsS https://api.videorayan.ir/v1/health \
  && curl -fsS https://en.videorayan.ir/api/v1/health \
  && curl -fsS https://api.videorayan.ir/v1/openapi.yaml -o /dev/null -w "openapi: %{http_code}\n"
# {"status":"ok"} {"status":"ok"} openapi: 200

Same-origin note: on *.videorayan.ir hosts, /api/v1/* proxies to the API — browsers avoid CORS entirely.

Errors & Limits

Status codes you should handle

StatusMeaningClient action
401Missing or invalid tenant keyCheck Authorization header / rotate key
403Not allowed (wrong room password; not room owner)Prompt for password / verify ownership
404Not found — includes cross-tenant probes (anti-enumeration)Refresh the link; don't retry blindly
409Room full or already endedSurface "room is full / has ended" to the user
400Validation failed — {"error":"invalid_input"} (lengths, unknown fields)Fix the payload; errors are deterministic
429Rate limited (120/min per key or IP)Back off exponentially; honor Retry-After

Token TTLs

Guest & tenant tokens: default 10 min, min 30 s, max 1 h. Mint fresh ones per session; treat them as credentials.

Room TTLs

Tenant rooms default 2 h (max 24 h). Free public rooms stay joinable for 12 h by default. Expired rooms are swept every 60 s.

Request size

Bodies are capped at 64 KB. Keep metadata small — it is for correlation, not storage.

Ready to build?

Start with the free Public API, upgrade to tenant rooms when you need ownership and roles — same client code either way.