# 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](/llms.txt) | The index: what JustOnAir is and a link to every page, in the [llms.txt](https://llmstxt.org) format. Read this first. |
| [/llms-full.txt](/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](/quickstart.md). Requests with `Accept: text/markdown` get Markdown at the normal URL too. |
| [/openapi.json](/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.

> **Note:** A hosted MCP server (create, read and end streams as MCP tools, with your key) is the next thing we are building. Until it ships, use the OpenAPI spec or the tool definitions below.

## 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](https://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

```text
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.
- `status` turns `live` the 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](/webhooks) send `stream.live`, `stream.ended` and `recording.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:

```json
[
  {
    "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`:

```ts
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_key` and `whip_url` as 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_url` is 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-Key` and you get the same stream back.
- **Check the budget before long events.** `GET /v1/usage` returns `available_usd`. Live streams end when it reaches zero.
- **Read `error.code`, not the message.** Codes are stable and listed on [Errors](/errors).

## Paste this into your agent's instructions

```text
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](/pricing).
- **Docs made to be read by models.** Markdown for every page, one-file `llms-full.txt`, OpenAPI 3.1, and stable error codes.
