JustOnAir docs
View as Markdown

API reference

Every endpoint, parameter and field of the JustOnAir API, generated from openapi.json.

All endpoints live at https://api.justonair.com. Authenticate with Authorization: Bearer <API key>. Requests and responses are JSON. Times are ISO 8601 UTC; money is a decimal string in USD.

This page is generated from openapi.json. Import that file into Postman, an SDK generator or an agent framework to get the same thing as code.

Streams

Create, read, change and end streams.

Create a stream

HTTP
POST /v1/streams

Reserves ingest capacity for 30 minutes and returns everything needed to go live: rtmp_url + stream_key, whip_url, playback_url and embed_url. The secrets are returned only here. Send an Idempotency-Key so a retry never creates a second stream.

ParameterInTypeDescription
Idempotency-KeyheaderstringAny unique string, e.g. a UUID. A retry with the same key and body returns the first response (header Idempotent-Replayed: true) for 24 h.

Body (optional)

FieldTypeDescription
namestring | nullUp to 100 characters. Blank means no name.
profile"abr_basic" | "passthrough"Default abr_basic.
max_resolution480 | 720 | 1080The tallest video you will send. Default 720. 1080 needs a trusted account. Sending more than you declare ends the stream after a 20 s warning.
recordbooleanMake an MP4 after the stream ends. Default true.
recording_retention_daysinteger | nullDays to keep the MP4, 1–3650. Omit for the account default (30). Null: keep until deleted.
metadataobjectYour own JSON, up to 2 KB.
playerPlayerSettings

Responses

StatusBodyMeaning
201CreatedStreamCreated. Store stream_key and whip_url now.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).
402ErrorCredit below the 1.00 USD minimum (insufficient_credit).
403ErrorAccount pending approval or suspended (account_pending, tenant_suspended).
422ErrorThis Idempotency-Key was used with a different body (idempotency_key_reused).
429ErrorAn account limit: limit_pending_streams, limit_creation_rate, limit_ingest_hours, limit_resolution.
503ErrorNo ingest capacity right now (no_capacity). Retry shortly.

List streams

HTTP
GET /v1/streams

Newest first.

ParameterInTypeDescription
limitqueryinteger
starting_afterquerystringId of the last stream on the previous page.
statusquery"pending" | "live" | "ended" | "expired" | "cancelled"

Responses

StatusBodyMeaning
200StreamListA page of streams.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).
403ErrorAccount pending approval or suspended (account_pending, tenant_suspended).

Get a stream

HTTP
GET /v1/streams/{id}

Status, what is arriving (ingest), the recording, and cost so far. Poll it to follow a stream; every 3–5 s is fine.

ParameterInTypeDescription
idpathstringStream id.

Responses

StatusBodyMeaning
200StreamThe stream.
401ErrorMissing or invalid API key (unauthorized).
403ErrorAccount pending approval or suspended (account_pending, tenant_suspended).
404ErrorNo such stream in your account (not_found).

Update a stream

HTTP
PATCH /v1/streams/{id}

Rename it, replace its metadata, or change player settings. Works in any status.

ParameterInTypeDescription
idpathstringStream id.

Body. Only these fields can change, in any status. Null clears a field; metadata is replaced whole.

FieldTypeDescription
namestring | nullUp to 100 characters.
metadataobject | null
playerPlayerSettings

Responses

StatusBodyMeaning
200StreamThe stream.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).
403ErrorAccount pending approval or suspended (account_pending, tenant_suspended).
404ErrorNo such stream in your account (not_found).

End or cancel a stream

HTTP
DELETE /v1/streams/{id}

Live: ends it and disconnects the publisher within seconds. Pending: cancels it and releases the slot. Already finished: returns it unchanged.

ParameterInTypeDescription
idpathstringStream id.

Responses

StatusBodyMeaning
200StreamThe stream, now ended or cancelled.
401ErrorMissing or invalid API key (unauthorized).
403ErrorAccount pending approval or suspended (account_pending, tenant_suspended).
404ErrorNo such stream in your account (not_found).

Replace a lost stream key

HTTP
POST /v1/streams/{id}/stream-key

Only while pending. The old key and WHIP URL stop working at once.

ParameterInTypeDescription
idpathstringStream id.

Responses

StatusBodyMeaning
200CreatedStreamThe stream with new stream_key and whip_url.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such stream in your account (not_found).
409ErrorLive or finished (stream_not_pending).

Viewers per minute

HTTP
GET /v1/streams/{id}/viewers

The viewer curve: live estimates from the hosted player, and exact counts from the CDN logs about an hour later.

ParameterInTypeDescription
idpathstringStream id.

Responses

StatusBodyMeaning
200ViewerSeriesPer-minute counts from both sources.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such stream in your account (not_found).

Playback

Signed URLs for your own player.

Sign a playback URL

HTTP
POST /v1/streams/{id}/playback-token

A fresh signed HLS URL, for example a short one per viewer minted by your backend. Refused once the stream is finished.

ParameterInTypeDescription
idpathstringStream id.

Body (optional)

FieldTypeDescription
expires_inintegerSeconds. Default: until the stream could no longer be live, plus an hour.

Responses

StatusBodyMeaning
200PlaybackTokenSigned URL.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such stream in your account (not_found).
409ErrorThe stream has ended (stream_ended). There is no replay.

Recordings

The MP4 made after a stream ends.

Delete the recording

HTTP
DELETE /v1/streams/{id}/recording

Deletes the MP4 now, or marks it to be deleted as soon as it exists.

ParameterInTypeDescription
idpathstringStream id.

Responses

StatusBodyMeaning
200StreamThe stream; recording.status is deleted or deleting.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such stream in your account (not_found).
HTTP
POST /v1/streams/{id}/recording/download
ParameterInTypeDescription
idpathstringStream id.

Body (optional)

FieldTypeDescription
expires_ininteger

Responses

StatusBodyMeaning
200RecordingDownloadA signed link to the MP4.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such stream in your account (not_found).
409ErrorNot ready (recording_not_ready, with details.status).

Account

Balance and usage.

Balance and usage

HTTP
GET /v1/usage
ParameterInTypeDescription
daysqueryinteger

Responses

StatusBodyMeaning
200UsageBalance, rates and usage per UTC day.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).

Webhooks

Endpoints that get stream and recording events, and their delivery log.

List webhook endpoints

HTTP
GET /v1/webhooks

Without secrets. Also returns event_types, every type an endpoint can subscribe to.

Responses

StatusBodyMeaning
200WebhookEndpointListYour endpoints.
401ErrorMissing or invalid API key (unauthorized).

Add a webhook endpoint

HTTP
POST /v1/webhooks

Up to 5 per account. The answer includes secret; see Webhooks for verifying signatures.

Body

FieldTypeDescription
urlstringhttps URL on the public internet, up to 2000 characters.
eventsarray of "stream.live" | "stream.ended" | "stream.expired" | "stream.cancelled" | "recording.ready" | "recording.failed" | "recording.deleted"Leave out for all events, including ones added later.
descriptionstringUp to 200 characters.

Responses

StatusBodyMeaning
201WebhookEndpointThe endpoint, with its secret.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).
429Error5 endpoints already (limit_webhook_endpoints).

Get a webhook endpoint

HTTP
GET /v1/webhooks/{id}

With its secret.

ParameterInTypeDescription
idpathstringEndpoint id (whk_…).

Responses

StatusBodyMeaning
200WebhookEndpointThe endpoint.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such endpoint (not_found).

Change a webhook endpoint

HTTP
PATCH /v1/webhooks/{id}
ParameterInTypeDescription
idpathstringEndpoint id (whk_…).

Body

FieldTypeDescription
urlstring
eventsarray of "stream.live" | "stream.ended" | "stream.expired" | "stream.cancelled" | "recording.ready" | "recording.failed" | "recording.deleted"
descriptionstring | null
enabledbooleanFalse stops sending; queued deliveries fail.

Responses

StatusBodyMeaning
200WebhookEndpointThe endpoint.
400ErrorInvalid request (invalid_request).
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such endpoint (not_found).

Delete a webhook endpoint

HTTP
DELETE /v1/webhooks/{id}

Its delivery log goes with it.

ParameterInTypeDescription
idpathstringEndpoint id (whk_…).

Responses

StatusBodyMeaning
204Deleted.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such endpoint (not_found).

Rotate the signing secret

HTTP
POST /v1/webhooks/{id}/secret

The old secret keeps signing alongside the new one for 24 hours.

ParameterInTypeDescription
idpathstringEndpoint id (whk_…).

Responses

StatusBodyMeaning
200WebhookEndpointThe endpoint with the new secret.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such endpoint (not_found).

Send a test event

HTTP
POST /v1/webhooks/{id}/test

Queues a webhook.test; it is sent within a couple of seconds.

ParameterInTypeDescription
idpathstringEndpoint id (whk_…).

Responses

StatusBodyMeaning
202WebhookDeliveryThe queued delivery.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such endpoint (not_found).

Delivery log

HTTP
GET /v1/webhooks/{id}/deliveries

Newest first, kept 30 days.

ParameterInTypeDescription
idpathstringEndpoint id (whk_…).
limitqueryinteger
statusquery"pending" | "succeeded" | "failed"

Responses

StatusBodyMeaning
200WebhookDeliveryListDeliveries, newest first.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo such endpoint (not_found).

Send a delivery again

HTTP
POST /v1/webhooks/{id}/deliveries/{delivery_id}/retry

A failed delivery gets one more attempt now; a pending one is brought forward.

ParameterInTypeDescription
idpathstringEndpoint id (whk_…).
delivery_idpathstringdlv_…

Responses

StatusBodyMeaning
202WebhookDeliveryThe delivery.
401ErrorMissing or invalid API key (unauthorized).
404ErrorNo pending or failed delivery with that id (not_found).

Embed

The hosted player’s public read.

Public stream status (hosted player)

HTTP
GET /v1/embed/{id}

No key needed; any web page may call it. Cached for a few seconds; the same read is served, cached at the CDN edge, at https://play.joacdn.com/api/embed/{id}. Rate limited per IP on unknown ids (60/min).

No API key needed.

ParameterInTypeDescription
idpathstringStream id.

Responses

StatusBodyMeaning
200EmbedStatus and, while live, a signed URL.
404ErrorUnknown stream.
429ErrorToo many requests (rate_limited).

Report a viewer (sampled)

HTTP
POST /v1/embed/{id}/beat

Sent by the hosted player, or by your own player to be counted live. Draw u once per page load; report every beacon.interval_seconds while u is below beacon.rate from the embed read. No body, so navigator.sendBeacon(url) works from any page.

No API key needed.

ParameterInTypeDescription
idpathstringStream id.
squerystringRandom id for this page load.
stquery"w" | "p" | "z" | "b"Waiting, playing, paused or buffering.
uquerynumberThe random draw, 0 ≤ u < 1.

Responses

StatusBodyMeaning
204Counted (or the stream is over and there is nothing to count).
400ErrorInvalid request (invalid_request).
404ErrorUnknown stream.
429ErrorToo many requests (rate_limited).

Objects

Stream

FieldTypeDescription
idstringStream id. Also the folder of its video on the CDN.
objectstringAlways stream.
namestring | nullYour label, up to 100 characters. Shown on the hosted player unless player.show_name is false.
status"pending" | "live" | "ended" | "expired" | "cancelled"pending (waiting for the publisher, up to 30 min), live, ended, expired (nothing connected in time) or cancelled (deleted before going live).
profile"abr_basic" | "passthrough"abr_basic: several qualities for viewers. passthrough: exactly what you send.
recordbooleanWhether an MP4 is made after the stream ends.
recordingRecording
node_readybooleanThe ingest server is up. False for about a minute when a new server has to start.
rtmp_urlstring | nullRTMP server URL for OBS or ffmpeg. Use with stream_key.
playback_urlstring | nullSigned HLS URL for your own player. Null once the stream is finished (there is no replay). Re-signed on every read.
playback_url_expires_atstring | nullWhen playback_url stops working.
embed_urlstringThe hosted player page. Put it in an iframe or share it as a link.
renditionsarray of stringQualities viewers will get, e.g. ["source", "480p"].
max_ingest_resolutionintegerThe tallest video you declared you will send: 480, 720 or 1080.
max_duration_secondsintegerThe stream ends after this long live. 4 h for new accounts, 24 h for trusted ones.
metadataobject | nullYour own JSON, up to 2 KB. Never shown to viewers.
playerPlayerSettings
reservation_expires_atstring | nullWhile pending: when the reserved slot is released and the stream expires.
reservation_expires_in_secondsinteger | nullThe same as a countdown, computed by the server.
created_atstring
went_live_atstring | null
ended_atstring | null
end_reasonstring | nullWhy it ended: deleted, disconnected, duration_limit, reservation_ttl, limit_bitrate, limit_resolution, insufficient_credit, simulcast_not_supported, provision_failed.
ingestIngest
costCost
viewersViewers
viewers_nowinteger | nullWatching on the hosted player right now (the same estimate as viewers.now.watching). Null unless the stream is pending or live and someone is on the player. Also in the list, so one call shows every live audience.

CreatedStream

A stream plus its secrets. Returned once, by the create call (and by replacing the key). Store them; they are never shown again.

FieldTypeDescription
idstringStream id. Also the folder of its video on the CDN.
objectstringAlways stream.
namestring | nullYour label, up to 100 characters. Shown on the hosted player unless player.show_name is false.
status"pending" | "live" | "ended" | "expired" | "cancelled"pending (waiting for the publisher, up to 30 min), live, ended, expired (nothing connected in time) or cancelled (deleted before going live).
profile"abr_basic" | "passthrough"abr_basic: several qualities for viewers. passthrough: exactly what you send.
recordbooleanWhether an MP4 is made after the stream ends.
recordingRecording
node_readybooleanThe ingest server is up. False for about a minute when a new server has to start.
rtmp_urlstring | nullRTMP server URL for OBS or ffmpeg. Use with stream_key.
playback_urlstring | nullSigned HLS URL for your own player. Null once the stream is finished (there is no replay). Re-signed on every read.
playback_url_expires_atstring | nullWhen playback_url stops working.
embed_urlstringThe hosted player page. Put it in an iframe or share it as a link.
renditionsarray of stringQualities viewers will get, e.g. ["source", "480p"].
max_ingest_resolutionintegerThe tallest video you declared you will send: 480, 720 or 1080.
max_duration_secondsintegerThe stream ends after this long live. 4 h for new accounts, 24 h for trusted ones.
metadataobject | nullYour own JSON, up to 2 KB. Never shown to viewers.
playerPlayerSettings
reservation_expires_atstring | nullWhile pending: when the reserved slot is released and the stream expires.
reservation_expires_in_secondsinteger | nullThe same as a countdown, computed by the server.
created_atstring
went_live_atstring | null
ended_atstring | null
end_reasonstring | nullWhy it ended: deleted, disconnected, duration_limit, reservation_ttl, limit_bitrate, limit_resolution, insufficient_credit, simulcast_not_supported, provision_failed.
ingestIngest
costCost
viewersViewers
viewers_nowinteger | nullWatching on the hosted player right now (the same estimate as viewers.now.watching). Null unless the stream is pending or live and someone is on the player. Also in the list, so one call shows every live audience.
stream_keystringRTMP stream key, {id}?key={secret}. OBS: paste as Stream Key with rtmp_url as Server. One-shot: works for this stream only.
whip_urlstringWHIP endpoint for browsers and WebRTC encoders. Contains the secret.

Ingest

What the ingest node last measured arriving. Null until the first measurement.

FieldTypeDescription
widthinteger | nullFrame width in pixels.
heightinteger | nullFrame height in pixels.
kbpsinteger | nullBitrate of everything received (video, audio, overhead), averaged over about 30 s.
observed_atstringWhen this was measured.
limit_warningstring | nullSet while a limit is broken: limit_bitrate or limit_resolution. Still broken 20 s later, the stream ends with that end_reason.
keyframe_interval_secondsnumber | nullAverage seconds between the publisher’s keyframes. Null for browser (WHIP) publishers.
keyframe_warningbooleanTrue above 2.5 s. Set your encoder to a 2 s keyframe interval. A warning only; never ends a stream.

Recording

The recording: one MP4 of the top quality, made after the stream ends. Null while pending.

FieldTypeDescription
status"recording" | "processing" | "ready" | "deleting" | "deleted" | "failed" | "not_recorded" | "none" | "legacy"recording while live, then processing, ready, deleting, deleted, failed, not_recorded (record: false) or none (never went live).
bytesinteger | nullMP4 size.
duration_secondsnumber | nullMP4 length.
retention_daysinteger | nullDays kept after it is ready. Null: until you delete it.
ready_atstring | nullWhen the MP4 became downloadable.
delete_atstring | nullWhen it will be deleted: the retention date, or 7 days after the balance reached zero, whichever is earlier.
delete_reasonstring | nullretention or zero_balance.
deleted_atstring | nullWhen it was deleted.

Cost

What this stream has cost so far at current rates. Only on GET /v1/streams/{id}. Amounts are decimal strings.

FieldTypeDescription
total_usdstringEverything below.
delivery_bytesintegerBytes delivered to viewers, counted up to delivery_counted_until.
delivery_usdstringDelivery cost.
delivery_counted_untilstring | nullCDN logs are read an hour at a time, so delivery trails reality by up to about 80 minutes.
ingest_secondsintegerSeconds of publishing.
ingest_usdstringEncoding (abr_basic) or passthrough cost.
storage_gb_hoursnumberRecording storage used.
storage_usdstringRecording storage cost.

Viewers

Viewer counts. While the stream runs, now is estimated from the hosted player: exact up to about 400 viewers, within about 5% above. After the fact, the CDN logs give the exact count for every player (hosted, your own, apps), about an hour later; peak and watch_minutes use them once they exist. Only on GET /v1/streams/{id}.

FieldTypeDescription
nowobject | nullLive estimate from the hosted player. Null when nobody is on the player or the stream is over.
peakinteger | nullMost viewers in any one minute.
peak_atstring | nullThe start of that minute.
watch_minutesinteger | nullTotal minutes watched, all viewers together.
source"cdn" | "player" | nullWhere peak and watch_minutes come from: cdn (every player, exact) or player (hosted player estimate, until the CDN count arrives).
cdn_counted_untilstring | nullCDN logs are read up to this time.

ViewerSeries

FieldTypeDescription
objectstringAlways viewer_series.
stream_idstringStream id. Also the folder of its video on the CDN.
playerarray of objectPer minute, from the hosted player (live estimate).
cdnarray of objectPer minute, from the CDN logs: average viewers watching at once, every player included.
countriesarray of objectWhere the watch time came from, from the CDN logs (every player), largest first, up to 30. Grows hour by hour while the stream runs.

PlayerSettings

FieldTypeDescription
show_namebooleanShow the stream name to viewers on the hosted player. Default true. False keeps the name out of the public embed read entirely.

StreamList

FieldTypeDescription
objectstringAlways list.
dataarray of Stream
has_morebooleanMore streams after the last one. Pass its id as starting_after.

PlaybackToken

FieldTypeDescription
objectstringAlways playback_token.
stream_idstringStream id. Also the folder of its video on the CDN.
playback_urlstringSigned HLS URL.
expires_atstring | null

RecordingDownload

FieldTypeDescription
objectstringAlways recording_download.
stream_idstringStream id. Also the folder of its video on the CDN.
urlstringSigned link to the MP4 on the CDN. Downloading it counts as delivery.
bytesintegerMP4 size.
expires_atstring

Embed

What the hosted player needs. Public: no key, any origin may read it.

FieldTypeDescription
objectstringAlways embed.
idstringStream id. Also the folder of its video on the CDN.
status"pending" | "live" | "ended" | "unavailable"pending, live, ended, or unavailable (expired or cancelled).
titlestring | nullThe stream name when it is to be shown, otherwise null.
playback_urlstring | nullWhile live: a signed HLS URL, the same for every viewer.
playback_url_expires_atstring | null
went_live_atstring | null
ended_atstring | null
poll_after_secondsinteger | nullAsk again after this many seconds (5 pending, 60 live). Null: stop asking.
beaconobject | nullViewer counting: a player whose random number is below rate reports every interval_seconds (see Count viewers from your own player). Null once the stream is over.

Usage

FieldTypeDescription
objectstringAlways usage.
balance_usdstringCredit minus everything charged so far.
available_usdstringBalance minus used-but-not-yet-charged. Live streams end when this reaches zero.
balance_depleted_atstring | nullSet while the balance is at or below zero.
recordings_delete_atstring | null7 days after the balance reached zero: when recordings will be deleted unless you top up.
ratesobject
daysarray of object

WebhookEndpoint

FieldTypeDescription
objectstringAlways webhook_endpoint.
idstringwhk_…
urlstringWhere events are POSTed. https only.
descriptionstring | nullYour own note.
eventsarray of "stream.live" | "stream.ended" | "stream.expired" | "stream.cancelled" | "recording.ready" | "recording.failed" | "recording.deleted"Event types sent to this endpoint.
all_eventsbooleanTrue when subscribed to everything, including event types added later.
enabledbooleanFalse: nothing is sent.
created_atstring
secretstringwhsec_…, for verifying signatures. Only on create, GET /v1/webhooks/{id} and rotation.

WebhookDelivery

FieldTypeDescription
objectstringAlways webhook_delivery.
idstringdlv_…
event_idstringevt_…, the webhook-id header.
event_typestring
stream_idstring | null
status"pending" | "succeeded" | "failed"
attemptsinteger
max_attemptsinteger
next_attempt_atstring | null
last_attempt_atstring | null
last_status_codeinteger | nullWhat your server answered.
last_errorstring | nullWhy the last attempt failed (answered 500, a timeout, a DNS error, …).
last_duration_msinteger | null
delivered_atstring | null
created_atstring
payloadWebhookEvent

WebhookEvent

The body of every webhook. Headers: webhook-id, webhook-timestamp, webhook-signature (Standard Webhooks).

FieldTypeDescription
type"stream.live" | "stream.ended" | "stream.expired" | "stream.cancelled" | "recording.ready" | "recording.failed" | "recording.deleted" | "webhook.test"
timestampstringWhen it happened.
dataobjectFor stream and recording events, the stream at that moment. For webhook.test, the endpoint.

Error

FieldTypeDescription
errorobject