REST API Design
Base URL:
https://api.example.com/v1
The v0.1 API is JSON over HTTPS.
1. Authentication
Customer backend requests:
Authorization: Bearer sk_live_xxxxxxxxx
Secret API keys are project-scoped.
Client applications must never receive a secret key.
2. Common headers
Request:
Authorization
Content-Type: application/json
Idempotency-Key
X-Request-Id optional
Response:
X-Request-Id
All create endpoints should support Idempotency-Key. Keys are scoped to the caller (API key or dashboard user) within a project and kept for 24 hours; a retry with the same key and body returns the stored response.
3. Error envelope
{
"error": {
"code": "room_not_found",
"message": "Room was not found.",
"request_id": "req_01K...",
"details": {}
}
}
Stable machine-readable error codes are part of the public contract.
Current error codes include:
invalid_request
authentication_failed
permission_denied
rate_limited
insufficient_credit
quota_exceeded
room_not_found
room_expired
room_ended
participant_not_found
participant_token_invalid
participant_token_expired
api_key_not_found
api_key_limit_reached
webhook_endpoint_not_found
webhook_endpoint_limit_reached
conflict
idempotency_in_progress
internal_error
service_unavailable
4. Pagination
List endpoints (GET /v1/rooms, /v1/billing/ledger, /v1/billing/topups,
/v1/audit-logs) return the newest items first and take one parameter:
GET /v1/rooms?limit=50
limit is 1 to 100 (default 50); anything else is 400 invalid_request.
There is no cursor yet: other query parameters (such as after or
status) are ignored, and only the newest limit items can be read.
Response:
{
"data": [],
"page": {
"has_more": false,
"next_cursor": null
}
}
has_more is true only when items exist beyond the ones returned (a page
that is exactly full reports false). next_cursor appears on the rooms
list only and is always null; do not loop on has_more expecting a
cursor.
5. Rooms
Create room
POST /v1/rooms
Request:
{
"name": "consult-938283",
"max_participants": 2,
"expires_in": 3600,
"region": "global",
"metadata": {
"booking_id": "BK000123"
}
}
Response:
{
"id": "room_01K...",
"name": "consult-938283",
"status": "waiting",
"region": "global",
"max_participants": 2,
"metadata": {
"booking_id": "BK000123"
},
"created_at": "2026-09-28T15:00:00Z",
"expires_at": "2026-09-28T16:00:00Z"
}
Rules:
- name is optional and need not be globally unique
- project_id comes from the authenticated API key
- region defaults to project default and must equal the server region
(
LIVEKIT_REGION, defaultglobal); any other value returns 400. - metadata size must be limited
- expires_in must have minimum/maximum bounds
- room IDs are globally unique and opaque
Get room
GET /v1/rooms/{room_id}
List rooms
GET /v1/rooms?limit=50
Newest first, all statuses; filter on status client-side.
End room
DELETE /v1/rooms/{room_id}
Ending a room:
- marks it ending
- requests media-engine room closure
- closes active participant sessions after reconciliation
- finalizes usage
- becomes ended
The operation is idempotent.
6. Participants
Create participant session
POST /v1/rooms/{room_id}/participants
Request:
{
"external_id": "customer_991",
"name": "Customer",
"role": "publisher",
"expires_in": 3600,
"metadata": {
"user_type": "customer"
}
}
Response:
{
"id": "part_01K...",
"room_id": "room_01K...",
"token": "rtc_pt_xxxxxxxxx",
"expires_at": "2026-09-28T16:00:00Z"
}
The returned token is a platform participant token.
Get participant
GET /v1/rooms/{room_id}/participants/{participant_id}
List participants
GET /v1/rooms/{room_id}/participants
Remove participant
DELETE /v1/rooms/{room_id}/participants/{participant_id}
7. Client connect exchange
Client SDK calls:
POST /v1/connect
Request:
{
"token": "rtc_pt_xxxxxxxxx",
"sdk": {
"name": "flutter",
"version": "0.1.0",
"platform": "ios"
}
}
Response example:
{
"session_id": "cs_01K...",
"endpoint": "wss://rtc.kallo.live",
"media_token": "short_lived_internal_token",
"expires_at": "2026-09-28T15:05:00Z",
"room": {
"id": "room_01K..."
},
"participant": {
"id": "part_01K..."
},
"features": {
"audio": true,
"video": true,
"screen_share": true
}
}
The SDK should hide media_token from normal application code where possible.
Connect endpoint checks:
- token signature/hash
- token expiry
- project status
- room status
- participant status
- participant limit
- project quota
- account credit policy
- region/media-node availability
8. Usage
Summary
GET /v1/usage/summary?from=2026-09-01&to=2026-09-30
Response:
{
"project_id": "prj_...",
"period": {
"from": "2026-09-01T00:00:00Z",
"to": "2026-10-01T00:00:00Z"
},
"participant_seconds": 10943545,
"participant_minutes": 182392.42,
"rooms": 3402,
"participants": 8120,
"finalized_only": true
}
Daily usage
GET /v1/usage/daily?from=2026-09-01&to=2026-09-30
9. Credit/billing
Balance
GET /v1/billing/balance
Response:
{
"currency": "USD",
"available": "4281.42",
"reserved": "0.00"
}
Ledger
GET /v1/billing/ledger?limit=50
Pricing and credit policy
GET /v1/billing/pricing
{
"currency": "USD",
"participant_minute_price": "0.006990",
"low_credit_threshold": "100.000000",
"minimum_credit_to_start": "1.000000",
"credit_state": "normal",
"billing_enabled": true
}
Online top-up
Create:
POST /v1/billing/topups
Authorization: Bearer sk_live_xxx
Idempotency-Key: topup-order-123
Content-Type: application/json
{
"amount": "1000.00",
"method": "checkout"
}
Required scope: billing:write.
method is optional: checkout (default) lets the payer choose among the
payment methods enabled in Stripe (cards, Apple Pay, Google Pay, Link);
card restricts it to cards. amount must be between 5.00 and 10000.00 USD
with at most 2 decimal places (400 invalid_request otherwise); larger
credits are arranged with sales. Send the payer to the
response's authorize_url (a Stripe Checkout page). See the top-ups guide.
Once a top-up has succeeded, receipt_url links its paid invoice or receipt
(empty before that).
Read:
GET /v1/billing/topups
GET /v1/billing/topups/{payment_id}
Only a provider-verified successful payment credits the wallet.
Customer API keys cannot mutate credit directly.
Beta top-ups use the operator/admin workflow. Active sessions use session-grace settlement: usage always settles even if the balance becomes negative, then new sessions are blocked until the credit gate is satisfied.
10. API keys
Console-authenticated endpoint:
POST /v1/api-keys
Request:
{
"name": "Production Backend",
"scopes": [
"rooms:read",
"rooms:write",
"participants:write",
"usage:read"
]
}
The environment is inherited from the project. A key can grant only scopes already held by the key creating it. This prevents privilege escalation.
An empty, unknown or oversized scopes list is 400 invalid_request;
asking for a valid scope the calling key does not hold is
403 permission_denied.
A project can have at most 50 active (unrevoked) keys; creating another
returns 409 api_key_limit_reached. Key creation is also rate limited to
20 per minute per project (429 rate_limited).
Full secret is returned exactly once.
Response:
{
"id": "key_01K...",
"secret": "sk_live_xxxxxxxxx",
"last4": "9a2f",
"created_at": "..."
}
Additional API-key endpoints:
GET /v1/api-keys
DELETE /v1/api-keys/{key_id}
The currently authenticated key cannot revoke itself; rotate to a new key first.
11. Project limits
GET /v1/project/limits
Response:
{
"project_id": "prj_...",
"max_active_rooms": 100,
"max_active_participants": 200,
"max_participants_per_room": 10,
"room_creates_per_minute": 60,
"participant_creates_per_minute": 240,
"connect_requests_per_minute": 1000
}
Concurrency limits are enforced transactionally in PostgreSQL. Request-rate limits use Redis.
12. Webhook endpoints
Create endpoint
POST /v1/webhook-endpoints
Request:
{
"url": "https://customer.example.com/webhooks/rtc",
"events": [
"room.started",
"room.ended",
"participant.joined",
"participant.left"
]
}
Return a webhook signing secret once.
A project can have at most 16 active endpoints; creating another returns
409 webhook_endpoint_limit_reached (disable one with
DELETE /v1/webhook-endpoints/{endpoint_id} first). Endpoint creation is
rate limited to 20 per minute per project (429 rate_limited).
13. Webhook events
Current public events:
room.started
room.ended
participant.joined
participant.left
usage.finalized
credit.low
credit.exhausted
credit.restored
payment.succeeded
payment.failed
payment.expired
payment.canceled
payment.refunded
Webhook envelope:
{
"id": "evt_01K...",
"type": "participant.joined",
"created_at": "2026-09-28T15:00:00Z",
"project_id": "prj_01K...",
"data": {
"room_id": "room_01K...",
"participant_id": "part_01K..."
}
}
14. Webhook delivery policy
Initial retry schedule example:
0 sec
10 sec
60 sec
5 min
30 min
2 hr
6 hr
24 hr
Exact values should be configurable.
A successful delivery is any configured accepted 2xx response.
Keep:
- attempt number
- request timestamp
- response status
- response latency
- truncated response body
- next attempt time
- final state
15. Rate limiting
Current write/connect limits are:
room creation per project
participant creation per project
top-up creation per project
/v1/connect per project
/v1/connect per source IP abuse limit
API key creation per project, fixed 20/min
webhook endpoint create per project, fixed 20/min
Rate counters are stored in Redis.
A limited response returns:
HTTP/1.1 429 Too Many Requests
Retry-After: <seconds>
X-RateLimit-Limit: <limit>
X-RateLimit-Remaining: 0
Commercial values come from project_limits, not hard-coded plan logic
(the two fixed management limits above are abuse guards, not plan limits).
16. Idempotency
Supported for:
- create room
- create participant
- create API key
- create webhook endpoint
The key is scoped to the authenticated project.
Only successful 2xx responses are cached. Credit/rate/service failures therefore remain retryable.
Stored response bodies may contain participant/API-key secrets, so cached response bytes are encrypted with AES-256-GCM using a dedicated IDEMPOTENCY_ENCRYPTION_KEY; AAD binds ciphertext to project ID and idempotency key.
Reusing a key with a different method/path/body hash returns HTTP 409. A concurrent request using the same key receives idempotency_in_progress.
17. API versioning
v0.1 launches under:
/v1
Breaking changes require a new version or explicit compatibility strategy.
Do not expose media-engine versioning in public URLs.
Dashboard sessions
The web dashboard signs people in with an emailed one-time link or Google
and then calls this API with a dashboard session token (rtc_us_...)
instead of an API key. Server integrations should keep using API keys.
| Route | Auth | Purpose |
|---|---|---|
GET /v1/auth/providers | none | Which sign-in methods are enabled |
POST /v1/auth/email/start | none | Email a sign-in link (always 202, never reveals whether the address exists; rate limited per address+IP and per address). Optional invitation_token joins that workspace on sign-in |
POST /v1/auth/email/peek | none | Which address a link is for, without using it (the web app asks the person to confirm before signing in) |
POST /v1/auth/email/verify | none | Exchange the link token for a session; the response has joined_account when an invitation was accepted |
GET /v1/auth/google/url?state= | none | Google authorization URL |
POST /v1/auth/google | none | Exchange a Google code (and optional invitation_token) for a session |
POST /v1/auth/logout | session | Revoke the session |
GET /v1/me | session | User, accounts, roles and projects |
PATCH /v1/me | session | Update your display name |
POST /v1/auth/logout-all | session | Sign out of every session |
POST /v1/accounts | session | Create another workspace (owner; capped per user) |
PATCH /v1/accounts/{id} | owner/admin | Rename a workspace |
PATCH /v1/accounts/{id}/projects/{project_id} | owner/admin | Rename a project |
DELETE /v1/accounts/{id}/projects/{project_id} | owner | Archive a project: its API keys are revoked and webhooks disabled; 409 last_project or 409 project_has_active_rooms when not allowed |
POST /v1/accounts/{id}/projects | owner/admin | Create a project |
GET/PATCH/DELETE /v1/accounts/{id}/members[/{user_id}] | member / owner-admin | Team members; an account always keeps an owner |
GET/POST/DELETE /v1/accounts/{id}/invitations[/{invitation_id}] | owner/admin | Invitations, bound to the invitee's email (20 per hour per account and per user, 50 open per account) |
POST /v1/invitations/accept | session | Accept an invitation |
GET /v1/accounts/{id}/audit-logs | owner/admin | Workspace-level audit events (team, invitations, projects) |
Every project route also accepts a session when the request carries
X-Project-Id. Scopes then come from the role: owners and admins have all
scopes; members can run rooms and participants and read everything else, but
cannot top up, manage API keys or manage webhooks.
New people always get their own workspace unless they sign in from an invitation link addressed to them; an open invitation alone never replaces their workspace.