API basics
Authentication, requests and responses, ids, pagination, retries, rate limits and CORS. The conventions every endpoint follows.
Read this once; every endpoint in the API reference follows it.
Base URL and authentication
All endpoints live at https://api.justonair.com. Send your API key as a Bearer token:
curl https://api.justonair.com/v1/streams \
-H "Authorization: Bearer $JOA_API_KEY"API keys start with joa_live_. Create them in the dashboard under API keys; each is shown once. An account can have 20 active keys. A missing or wrong key gets 401 unauthorized.
Keep keys on your server. The API doesn't accept them from browsers (see CORS), and a key in a web page or an app would let anyone run streams on your credit. The public endpoints under /v1/embed/ need no key: they are what viewers' browsers call.
GET https://api.justonair.com/ needs no key and answers with links to these docs, openapi.json and llms.txt.
Requests and responses
- Bodies are JSON: send
Content-Type: application/json. Unknown fields are refused with400 invalid_request, which names the field, so a typo never passes silently. - On
PATCH, a field you leave out is unchanged andnullclears it. - Every object has an
objectfield saying what it is (stream,list,chat_message, …). - Times are ISO 8601 in UTC:
2026-10-09T18:05:12.000Z. - Money is a decimal string in USD, never a float:
"0.016790". Parse it with a decimal type. - Fields that are always there but have nothing to say are
null, not missing. New fields can appear at any time; ignore the ones you don't know. What changed when: Changelog.
Ids
Ids are strings with a prefix that says what they are. Treat them as opaque.
| Prefix | What |
|---|---|
str_ | Stream (also its folder on the CDN) |
msg_ | Chat message |
vsn_ | Viewer session |
ban_ | Chat ban |
mod_ | Chat moderator |
whk_ | Webhook endpoint |
dlv_ | Webhook delivery |
evt_ | Webhook event (the webhook-id header) |
whsec_ | Webhook signing secret |
req_ | Request id (see below) |
joa_live_ | API key |
Pagination
Lists answer {"object": "list", "data": [...], "has_more": true}, newest first. Pass limit (up to 100) and, for the next page, the id of the last item as starting_after:
curl "https://api.justonair.com/v1/streams?limit=100&starting_after=str_e1bzk3dxw9z9allei6n7" \
-H "Authorization: Bearer $JOA_API_KEY"The chat feed pages by position instead: pass the previous answer's next_after as after. See Chat and reactions.
Retries and idempotency
POST /v1/streams takes an Idempotency-Key header: any unique string, such as a UUID, up to 255 characters. A retry with the same key and the same body within 24 hours returns the first answer (with Idempotent-Replayed: true) instead of creating a second stream. The same key with a different body gets 422 idempotency_key_reused. Send one on every create; then a timeout is always safe to retry.
GET, PATCH and PUT are safe to repeat as they are: they read or set a state. Ending a stream that has already ended returns it unchanged. Other POSTs (a webhook endpoint, a moderator link, a playback token) take no key: on a timeout, list or read before you create again.
When an error goes away by waiting, the response has a Retry-After header (seconds) and usually error.details.retry_after_seconds: rate_limited, limit_creation_rate, no_capacity and the chat limits. Limits that waiting doesn't fix, such as limit_pending_streams, have none, so a generic client won't retry them in a loop. Retry a 5xx with backoff.
Rate limits
API-key calls have no per-key request limit today. What's limited is what you create: streams waiting and created per hour, ingest hours, and the rest on Limits, each with its own error code. Poll a stream every 3–5 seconds at most, or use webhooks instead.
The public endpoints under /v1/embed/ are limited per viewer address (429 rate_limited), and chat posts and reactions have their own limits: Chat limits.
If a per-key limit is added, it will answer 429 rate_limited with Retry-After, which clients that follow this page already handle.
Errors
Every error has the same shape, with a code that never changes:
{
"error": {
"code": "limit_pending_streams",
"message": "At most 2 streams may be waiting to go live at once. Start or delete one first.",
"details": { "limit": 2, "current": 2 },
"request_id": "req_8k2m0q4b7c1d8e3a9f5x"
}
}Program against code; show message to people. Every code: Errors. In openapi.json, each error response lists the codes it can carry in x-error-codes.
Request ids
Every response has an X-Request-Id header, and every error body has the same value in error.request_id. Log it, and quote it when you write to us: it finds your request in our logs.
CORS
| Endpoints | From a browser |
|---|---|
/v1/embed/… (player, chat, reactions, viewer sessions) | Any site, no cookies. |
| Everything that takes an API key | No: call them from your server. |
Webhooks
Instead of polling, get stream.live, stream.ended, recording.ready and more POSTed to your server, signed in the Standard Webhooks format. See Webhooks.