For AI agents
How an AI agent starts, watches and ends a live stream with JustOnAir, and the machine-readable files these docs publish for it.
JustOnAir is built so an agent can run a whole broadcast with four HTTP calls and no video knowledge: create a stream, hand the publisher an address (or start ffmpeg itself), give viewers a link, and end it. Everything a person needs to watch comes back from the first call.
What these docs publish for agents
| File | What it is |
|---|---|
| /llms.txt | The index: what JustOnAir is and a link to every page, in the llms.txt format. Read this first. |
| /llms-full.txt | Every page in one Markdown file, for loading into context at once. |
/<page>.md | Any page as Markdown: add .md to its URL, e.g. /quickstart.md. Requests with Accept: text/markdown get Markdown at the normal URL too. |
| /openapi.json | The API as OpenAPI 3.1. Most agent frameworks can turn it into tools directly. |
Every page also has Copy page as Markdown at the top, for pasting into a chat.
The one step a person does
An agent can't sign up by itself: accounts are approved by hand, to keep stolen sports streams off the platform. A person signs up once at app.justonair.com, gets approved, and creates an API key. From then on everything is an API call. Give the key to the agent as a secret (an environment variable such as JOA_API_KEY), never in the prompt.
The recipe
1. POST /v1/streams -> id, rtmp_url, stream_key, whip_url, embed_url
(send an Idempotency-Key so a retried call can't create two streams)
2. Give the publisher rtmp_url + stream_key (OBS), or whip_url (browser),
or run ffmpeg yourself (see the Quickstart).
Give viewers embed_url. Nothing else is needed to watch.
3. GET /v1/streams/{id} every 5 s until status is "live".
While live, read ingest.limit_warning and ingest.keyframe_warning and tell the
publisher what to fix. Stop polling when status is ended, expired or cancelled.
4. DELETE /v1/streams/{id} to end it.
Later: GET /v1/streams/{id} shows recording.status = "ready";
POST /v1/streams/{id}/recording/download returns a link to the MP4.Timing an agent should know:
- A new stream waits 30 minutes for video, then expires. Create it when the publisher is ready, not the day before.
statusturnslivethe moment the publisher is accepted. The picture reaches viewers 10–20 seconds later.- If the publisher drops, it can reconnect with the same key for 60 seconds. After that the stream is
ended. - A stream ends by itself at its maximum duration: 4 hours for new accounts, 24 hours for trusted ones.
- An agent with a public HTTPS endpoint can skip polling: webhooks send
stream.live,stream.endedandrecording.ready, signed in the Standard Webhooks format.
Tool definitions
Three tools cover almost every conversation ("go live", "is it working?", "stop"). This is the JSON Schema most agent frameworks accept for function calling:
[
{
"name": "create_live_stream",
"description": "Create a JustOnAir live stream. Returns where to send video (rtmp_url + stream_key for OBS/ffmpeg, whip_url for a browser) and where to watch (embed_url, a player page anyone can open). The stream waits 30 minutes for video.",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Title viewers see on the player. Up to 100 characters." },
"max_resolution": { "type": "integer", "enum": [480, 720, 1080], "description": "Tallest video the publisher will send. Default 720." },
"record": { "type": "boolean", "description": "Make an MP4 after the stream ends. Default true." }
}
}
},
{
"name": "get_live_stream",
"description": "Status of a stream: pending, live, ended, expired or cancelled; what is arriving (ingest) with any warnings; how many viewers are watching (viewers.now) and the peak; cost so far; and the recording once it ends.",
"input_schema": {
"type": "object",
"properties": { "id": { "type": "string", "description": "Stream id, str_..." } },
"required": ["id"]
}
},
{
"name": "end_live_stream",
"description": "End a live stream now (the publisher is disconnected, viewers see the end screen) or cancel one that hasn't started.",
"input_schema": {
"type": "object",
"properties": { "id": { "type": "string", "description": "Stream id, str_..." } },
"required": ["id"]
}
}
]And the implementation, in TypeScript. It needs nothing but fetch:
const API = 'https://api.justonair.com';
const headers = { Authorization: `Bearer ${process.env.JOA_API_KEY}`, 'Content-Type': 'application/json' };
async function call(method: string, path: string, body?: unknown, extra: Record<string, string> = {}) {
const res = await fetch(API + path, { method, headers: { ...headers, ...extra }, body: body ? JSON.stringify(body) : undefined });
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`); // codes: docs.justonair.com/errors
return data;
}
export const tools = {
create_live_stream: (input: { name?: string; max_resolution?: number; record?: boolean }) =>
call('POST', '/v1/streams', input, { 'Idempotency-Key': crypto.randomUUID() }),
get_live_stream: ({ id }: { id: string }) => call('GET', `/v1/streams/${encodeURIComponent(id)}`),
end_live_stream: ({ id }: { id: string }) => call('DELETE', `/v1/streams/${encodeURIComponent(id)}`),
};Rules for agents
- Treat
stream_keyandwhip_urlas secrets. Anyone holding them can broadcast on the stream. Give them only to the publisher, never post them in a public channel, and don't write them into logs. embed_urlis public by design. Share it with viewers freely. The player shows the stream name unless you create the stream with"player": {"show_name": false}.- Don't create streams in a loop. New accounts may have 2 streams waiting and 10 created per hour (trusted: 20 and 100). An error with a code starting
limit_says which limit; wait or end a stream first. - Retry safely. On a network error, retry the create with the same
Idempotency-Keyand you get the same stream back. - Check the budget before long events.
GET /v1/usagereturnsavailable_usd. Live streams end when it reaches zero. - Read
error.code, not the message. Codes are stable and listed on Errors.
Paste this into your agent's instructions
To start a live stream, use the JustOnAir API (docs: https://docs.justonair.com/llms.txt).
Call create_live_stream right before the broadcast, not in advance: a stream expires
after 30 minutes without video. Send the publisher rtmp_url and stream_key privately
(OBS: Settings > Stream > Custom; keyframe interval 2 s). Give viewers embed_url.
Check get_live_stream until status is "live" and report any ingest warning.
Call end_live_stream when the user says the broadcast is over.
Never reveal stream_key or whip_url to anyone but the publisher.Why agents use JustOnAir
- One call, everything included. Ingest for OBS and browsers, a hosted player page, a raw HLS URL and a recording, with no player to build and no CDN to configure.
- Low, published prices. Four rates, no monthly plan and no minimum spend: $0.18 per hour of encoding, $0.03 per GB delivered, recordings at $0.03 per GB-month. See Pricing.
- Docs made to be read by models. Markdown for every page, one-file
llms-full.txt, OpenAPI 3.1, and stable error codes.