---
url: https://live777.pages.dev/guide/live777-api.md
---
# Live777 HTTP API

## WHIP && WHEP

`POST` `/whip/:streamId`

Response: \[201]

If the stream already has a WHIP publisher, the new publish displaces it
(mediamtx-style override): the old session is closed with reason
`replaced`. Set `strategy.override_publisher = false` (globally or per
stream) to reject duplicate publishes with \[409] instead. A cascade-pull
publisher is never displaced and always conflicts with \[409].

`POST` `/whep/:streamId`

Response: \[201]

***

`PATCH` `/session/:streamId/:sessionId`

Response: \[204]

`DELETE` `/session/:streamId/:sessionId`

Response: \[204]

## Stream

### Create a Stream

`POST` `/api/streams/:streamId`

`streamId` need unique id

Maybe you can use this configuration auto create stream

```toml
[strategy]
# WHIP auto a stream
auto_create_whip = true
# WHEP auto a stream
auto_create_whep = true
```

You can also override any strategy field per stream:

```toml
[stream.restricted]
[stream.restricted.strategy]
auto_create_whip = false
auto_create_whep = false
```

Response: \[204]

### Get all Stream

`GET` `/api/streams/`

Response: \[200]

* `id`: String, `streamId`
* `createdAt`: Int, `timestamp`
* `publish`: `Object(PubSub)`, about publisher
* `subscribe`: `Object(PubSub)`, about subscriber
* `(publish | subscribe).leaveAt`: Int, `timestamp`
* `(publish | subscribe).sessions`: Array, `sessions`
* `(publish | subscribe).sessions.[].id`: String, `sessionId`
* `(publish | subscribe).sessions.[].createdAt`: Int, `timestamp`
* `(publish | subscribe).sessions.[].state`: String, [RTCPeerConnection/connectionState](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState#value)
* `(publish | subscribe).sessions.[].cascade`: Optional(Object(Cascade))
* `(publish | subscribe).sessions.[].cascade.sourceUrl`: Optional(String(URL))
* `(publish | subscribe).sessions.[].cascade.targetUrl`: Optional(String(URL))
* `(publish | subscribe).sessions.[].cascade.sessionUrl`: String(URL)
* `(publish | subscribe).sessions.[].stats`: `Object(Stats)`, media statistics for this session: inbound for a publish session, outbound for a subscribe session
* `(publish | subscribe).sessions.[].stats.bytes`: Int, cumulative bytes (RTP wire size: header + extensions + payload)
* `(publish | subscribe).sessions.[].stats.packets`: Int, cumulative packets
* `(publish | subscribe).sessions.[].stats.bitrate`: Int, current rate in bits per second, re-sampled every 2 seconds
* `stats`: `Object`, stream-level statistics: `stats.publish` is the inbound (publisher) aggregate, `stats.subscribe` the sum of all outbound subscribers; both use the same `Stats` shape, and their cumulative counters stay monotonic across republishes and subscriber churn
* `statsScope`: String, scope of `stats`; `node` for one liveion node, `clusterNodeWork` for liveman's sum of node-level work across nodes, where cascade hops are counted on each relay node

For Example:

```json
[
  {
    "id": "push",
    "createdAt": 1719326206862,
    "publish": {
      "leaveAt": 0,
      "sessions": [
        {
          "id": "08c1f2a0a60b0deeb66ee572bd369f80",
          "createdAt": 1719326206947,
          "state": "connected"
        }
      ]
    },
    "subscribe": {
      "leaveAt": 1719326206862,
      "sessions": []
    }
  },
  {
    "id": "pull",
    "createdAt": 1719326203854,
    "publish": {
      "leaveAt": 0,
      "sessions": [
        {
          "id": "41b2c52da4fb1eed5a3bff9a9a200d80",
          "createdAt": 1719326205079,
          "state": "connected",
          "cascade": {
            "sourceUrl": "http://localhost:7777/whep/web-0",
            "sessionUrl": "http://localhost:7777/session/web-0/aabc02240abfc7f4800e8d9a6f087808"
          }
        }
      ]
    },
    "subscribe": {
      "leaveAt": 1719326203854,
      "sessions": []
    }
  },
  {
    "id": "web-0",
    "createdAt": 1719326195910,
    "publish": {
      "leaveAt": 0,
      "sessions": [
        {
          "id": "0dc47d8da8eb0a64fe40f461f47c2a36",
          "createdAt": 1719326196264,
          "state": "connected"
        }
      ]
    },
    "subscribe": {
      "leaveAt": 0,
      "sessions": [
        {
          "id": "aabc02240abfc7f4800e8d9a6f087808",
          "createdAt": 1719326204997,
          "state": "connected"
        },
        {
          "id": "dab1a9e88b2400cfd4bcfb4487588ef3",
          "createdAt": 1719326206798,
          "state": "connected",
          "cascade": {
            "targetUrl": "http://localhost:7777/whip/push",
            "sessionUrl": "http://localhost:7777/session/push/08c1f2a0a60b0deeb66ee572bd369f80"
          }
        },
        {
          "id": "685beee8650b761116b581a4a87ca9b9",
          "createdAt": 1719326228314,
          "state": "connected"
        }
      ]
    }
  }
]
```

### Destroy a Stream

`DELETE` `/api/streams/:streamId`

Response: \[204]

Streams declared in the config file (`[stream.<name>]`) are provisioned and
cannot be deleted or recreated through the API: `DELETE` and the duplicate
`POST` return \[409] for them.

## Streams SSE

`GET` `/api/sse/streams`

Server-Sent Events endpoint. Requires the same auth as admin routes.

Pushes the full snapshot of all streams whenever the stream state changes. Each SSE message is a JSON array of stream objects:

```json
[
  {
    "id": "streamId",
    "publish": { ... },
    "subscribe": { ... },
    "reforward": { ... }
  }
]
```

Use this endpoint to keep a live view of the current stream state. The first message is sent when the connection is established; subsequent messages are sent on every state change, and the 2-second stats tick pushes a snapshot whenever it changed, so the reported bitrates refresh while media flows and decay to zero when a stream goes silent.

## Cascade

`POST` `/api/cascade/:streamId`

Request:

```json
{
  "token": "",
  "sourceUrl": "",
  "targetUrl": "",
}
```

* `token`: Option, auth header
* `sourceUrl`: `Option<WHEP url>`. if has, use pull mode
* `targetUrl`: `Option<WHIP url>`. if has, use push mode
* `sourceUrl` and `targetUrl` at the same time can only one

## Source

The bitrate endpoint is read-only *encoder* telemetry (native
capture/encoder sources only — see the [livehal guide](./livehal.md)):
how the source's bitrate is currently driven. The tier endpoint is the
control surface: it re-provisions the source with a named preset of
source parameters (capture geometry and/or encoder budget).

### Get Source Bitrate State

`GET` `/api/sources/:streamId/bitrate`

Reports how the bitrate of the stream's configured media source is
currently driven.

Response: \[200]

```json
{
  "stream_id": "pi-cam",
  "mode": "adaptive",
  "bitrate": 2000000,
  "adaptive": true
}
```

* `mode`: `adaptive` (AIMD controller) or `fixed` (configured value, or
  the last applied tier's).
* `bitrate`: current encoder bitrate; the configured value while the source
  is in standby (`null` for non-native sources).

Returns \[404] when the stream has no source and \[405] for any method
other than `GET`.

### Get Source Quality Tiers

`GET` `/api/sources/:streamId/tier`

Reports the quality tiers configured on the stream's source and which one
is active.

Response: \[200]

```json
{
  "stream_id": "pi-cam",
  "active_tier": "low",
  "tiers": [
    { "name": "mid", "bitrate": 2000000 },
    { "name": "low", "bitrate": 600000, "width": 640, "height": 480, "fps": 15 }
  ]
}
```

* `active_tier`: the last tier applied through this API (`null` = the
  configured base profile).
* `tiers`: each entry carries `bitrate` and, for ladder rungs,
  `width`/`height`/`fps` (see the [livehal guide](./livehal.md)).

Returns \[404] when the stream has no source.

### Apply Source Quality Tier

`POST` `/api/sources/:streamId/tier`

Switches the source to a configured quality tier:

```json
{ "tier": "low" }
```

Response: \[200]

```json
{ "stream_id": "pi-cam", "tier": "low", "bitrate": 600000, "rebuilt": true }
```

A bitrate-only tier retunes the running encoder in place (`rebuilt:
false`). A tier carrying `width`/`height`/`fps` rebuilds the
capture+encoder pipeline (`rebuilt: true`): subscribers stay connected
but observe a brief frame gap and an in-band SPS/PPS change. Either way
the adaptive controller is *not* suspended — the tier's bitrate becomes
its new ceiling, so the AIMD keeps following network conditions within
the rung.

* \[400] `tier` is missing or not defined for the stream.
* \[404] the stream has no source.
* \[409] the source is not running, or the pipeline rebuild failed (the
  previous configuration was rolled back).

## Recorder

### Start Recording a Stream

`POST` `/api/record/:streamId`

Starts recording the specified stream. The stream must be active (have a publisher) for recording to begin.

Request Body (optional):

```json
{ "base_dir": "optional/path/prefix" }
```

* `base_dir` (optional): override the storage path prefix. If omitted, Live777 uses `/:streamId/:record_id/` where `record_id` is the current Unix timestamp. Once a session reaches `max_recording_seconds`, a new timestamp directory is created automatically.

Response: \[200]

```json
{
  "id": "camera01",
  "record_id": "1718200000",
  "record_dir": "camera01/1718200000",
  "mpd_path": "camera01/1718200000/manifest.mpd"
}
```

`record_id` is an empty string only when the recorder cannot infer a 10-digit Unix timestamp from the output path (for example, when a custom `base_dir` omits that suffix).

### Recording Status (by id)

`GET` `/api/record/:streamId`

Response: \[200]

```json
{ "recording": true }
```

### Stop Recording

`DELETE` `/api/record/:streamId`

Stops an active recording session for the specified stream. Returns \[200] with an empty body on success.

Reference: [Recorder](recorder)
