JustOnAir docs
View as Markdown

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:

Shell
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 with 400 invalid_request, which names the field, so a typo never passes silently.
  • On PATCH, a field you leave out is unchanged and null clears it.
  • Every object has an object field 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.

PrefixWhat
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:

Shell
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:

JSON
{
  "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

EndpointsFrom a browser
/v1/embed/… (player, chat, reactions, viewer sessions)Any site, no cookies.
Everything that takes an API keyNo: 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.