> ## Documentation Index
> Fetch the complete documentation index at: https://docs.overshoot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The Stream

> A leased session that holds your live video feed and makes it addressable from any chat completion.

The core primitive in Overshoot is the `Stream`.

Clients connect any live video source to Overshoot via the `Stream`. Creating one is straightforward — call `/streams` with an API key:

```shellscript theme={null}
curl -X POST https://api.overshoot.ai/v1beta/streams \
  -H "Authorization: Bearer $OVERSHOOT_API_KEY"
```

This returns a `stream_id` and a [LiveKit room](https://docs.livekit.io/intro/basics/rooms-participants-tracks/rooms/). Use any [LiveKit SDK](https://docs.livekit.io/reference/) to join the room and publish your video into it. Our media gateway joins the same room, ingests your stream, and prepares it for inference.

<Accordion title="Sample response">
  ```json focus={2-3,5-9} theme={null}
  {
    "id": "2ea5a604-d225-4cd2-82ac-b907cb0b4f63",
    "state": "active",
    "publish": {
      "type": "livekit",
      "url": "wss://livekit.overshoot.ai",
      "token": "ey...k"
    },
    "expires_at_ms": 1777529931184,
    "ttl_seconds": 300
  }
  ```
</Accordion>

A `Stream` holds the state of your live feed **as long as it's alive**. Read on for how to keep one alive, and how to kill it.

## Stream state

The `Stream` holds your video as long as it's alive. If you push no frames, it sits empty.

To check the state (e.g. frames received, recent FPS, time to expiry, etc.), call:

```shellscript theme={null}
curl https://api.overshoot.ai/v1beta/streams/{stream_id} \
  -H "Authorization: Bearer $OVERSHOOT_API_KEY"
```

<Accordion title="Sample response">
  ```json theme={null}
  {
    "id": "2ea5a604-d225-4cd2-82ac-b907cb0b4f63",
    "state": "active",
    "stream_time_ms": 12480,
    "first_frame_at_ms": 1777529631184,
    "last_frame_at_ms":  1777529643664,
    "last_frame_index": 312,
    "recent_fps": 25.0,
    "retained_frame_count": 312,
    "evicted_frame_count": 0,
    "expires_at_ms": 1777529931184,
    "ttl_seconds": 300
  }
  ```
</Accordion>

A `Stream` ends when it expires (5 min idle) or gets `DELETE`d. Once `state` flips to `ended`, it stays ended — there is no resume.

## How to keep a stream alive

When Overshoot starts ingesting your stream, we run a fair amount of processing in the background to make every frame available for any model. To avoid leaking sessions, a stream will expire after 5 minutes. To keep it alive, call `/keepalive` with your `stream_id` regularly. Every 2 minutes is a safe cadence.

```shellscript theme={null}
curl -X POST https://api.overshoot.ai/v1beta/streams/{stream_id}/keepalive \
  -H "Authorization: Bearer $OVERSHOOT_API_KEY"
```

Each keepalive returns a fresh LiveKit token. Save it — if your publisher disconnects from the room, you'll need it to rejoin without recreating the stream.

## How to kill a stream

`DELETE` it.

```shellscript theme={null}
curl -X DELETE https://api.overshoot.ai/v1beta/streams/{stream_id} \
  -H "Authorization: Bearer $OVERSHOOT_API_KEY"
```

## Lifecycle, in detail

### States

A `Stream` has exactly two states:

* `active` — created, accepting publishers and inference. The default for any newly-created stream.
* `ended` — terminal. Set once on TTL expiry or explicit delete. Never transitions back.

`GET /streams/{id}` continues to return ended streams for a short tombstone window with `state: "ended"`, `ended_at_ms`, and `end_reason`. After the tombstone window the endpoint returns `404`.

### TTL and keepalive

* Default TTL: **300 seconds** (`ttl_seconds: 300`) from the most recent of: creation, keepalive, or last activity signal.
* Each successful `POST /keepalive` resets the lease to `now + ttl_seconds` and returns a new `expires_at_ms` plus a fresh LiveKit `token`.
* Recommended polling cadence: every 60–120s. Don't rely on TTL minus one second; clock skew between caller and server is real.
* A stream with `state: "ended"` cannot be revived by a keepalive. Create a new stream instead.

### Stream status fields

| Field                         | Type    | Notes                                                     |
| ----------------------------- | ------- | --------------------------------------------------------- |
| `id`                          | uuid    | Stable for the lifetime of the stream.                    |
| `state`                       | enum    | `active` \| `ended`.                                      |
| `stream_time_ms`              | float   | Stream-clock position in ms — monotonic from first frame. |
| `first_frame_at_ms`           | int     | Wall-clock ms of the first frame ingested.                |
| `last_frame_at_ms`            | int     | Wall-clock ms of the most recent frame.                   |
| `last_frame_index`            | int     | Lifetime-indexed position of the most recent frame.       |
| `first_available_frame_at_ms` | int     | Wall-clock ms of the oldest retained frame.               |
| `first_available_frame_index` | int     | Lifetime index of the oldest retained frame.              |
| `recent_fps`                  | float   | Rolling FPS over the last few seconds of ingest.          |
| `retained_frame_count`        | int     | Frames currently retained in the buffer.                  |
| `evicted_frame_count`         | int     | Frames that have aged out of retention.                   |
| `created_at_ms`               | int     | Wall-clock ms of stream creation.                         |
| `expires_at_ms`               | int     | Wall-clock ms when the lease will expire if not renewed.  |
| `ttl_seconds`                 | int     | Configured TTL — currently 300 for all streams.           |
| `ended_at_ms`                 | int?    | Set when `state == "ended"`.                              |
| `end_reason`                  | string? | `expired` \| `deleted`.                                   |
| `audio`                       | bool    | Reserved. Always `false`.                                 |

### Frame indexing semantics

`frame_index` is monotonically increasing across the stream's lifetime. It is *not* reset by eviction. If you reference a `frame_index` that's older than `first_available_frame_index`, the URL resolver clamps to `first_available_frame_index` (intersection-with-availability semantics) — your request succeeds against the oldest frame still in the buffer. The same applies to `start_frame_index` for video segments.

Negative indices are interpreted relative to `last_frame_index` at request time: `frame_index=-1` is "most recent", `-2` is the one before, and so on.

### Errors

| Code        | Meaning                                                                             |
| ----------- | ----------------------------------------------------------------------------------- |
| `401`/`403` | Missing or invalid API key.                                                         |
| `402`       | Insufficient credits to create a new stream.                                        |
| `404`       | Stream not found, ended past the tombstone window, or owned by a different API key. |
| `503`       | Ingest service draining or starting up — retry with backoff.                        |
