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

# AI live streaming API

> Create and manage APOB AI livestream channels through GraphQL: sources, HLS playback, audience scenes and votes, channel controls, credits, and replay.

Build a generated-video livestream into your application. Start a channel from text
or media, retrieve its HLS URL, add scenes, and let audience proposals and votes
influence what happens next. For a first request, follow the
[livestream quickstart](https://developers.apob.ai/guides/create-livestream-channel.md).

Send requests to your configured `APOB_GRAPHQL_URL` with
`Authorization: Bearer <APOB_API_KEY>`. Mutations use the verified account;
private-channel reads and owner controls require the owner. Public channels can be
read without login. See [API authentication](https://developers.apob.ai/api-reference/authentication.md).

> [!NOTE]
>
> **Channel** is the product term. The GraphQL contract keeps its existing
> names: `LiveRoom`, `createLiveRoom`, `liveRoom`, `roomId`, and
> `sourceLiveRoomId`. Use these names exactly as shown in requests and selections.

## Operations

These signatures show public arguments only:

```graphql theme={"system"}
createLiveRoom(input: CreateLiveRoomInput!): LiveRoom!
liveRoom(id: ID!): LiveRoom
listPublicLiveRooms(pageFetchingInfo: PageFetchingInfo!): LiveRoomPageInfo!
listPersonalLiveRooms(pageFetchingInfo: PageFetchingInfo!): LiveRoomPageInfo!
createLiveScene(input: CreateLiveSceneInput!): LiveScene!
voteLiveScene(input: VoteLiveSceneInput!): LiveSceneVoteResult!
setLiveRoomAudienceSceneCreationEnabled(id: ID!, enabled: Boolean!): LiveRoom!
stopLiveRoom(id: ID!): LiveRoom!
reopenLiveRoom(id: ID!, initialPrompt: String, initialLength: Long!): LiveRoom!
```

Channel reads and lists are queries; all other operations above are mutations.
Public channel visibility allows reads and eligible participation,
not ownership changes. Private channels are visible only to their owner.

## Create a channel

### CreateLiveRoomInput

| Input | Required | Contract |
| - | - | - |
| `initialPrompt` | Yes | Nonblank initial scene prompt, subject to moderation. |
| `initialLength` | Yes | `Long`, milliseconds, at least `15000`; rounded upward to a whole second. Prepays new generated scenes. |
| `resolution` | Yes | `P480` or `P768`. |
| `isPublic` | Yes | Boolean; choose public discovery or private owner access explicitly. |
| `audienceSceneCreationEnabled` | No | Boolean, defaults to `true`. |
| `sourceMediaId` | No | ID of accessible image/video media. |
| `sourceLiveRoomId` | No | Owned or public channel to use as a source. |
| `uploadedImage` | No | Uploaded image object: `url`, `width`, and `height` are required. |
| `uploadedVideo` | No | Object with a nonblank uploaded `url`; omit `mediaId`. Live measures and uses the full video; width, height, duration, and `sliceInfo` are not applied. |

Supply at most one source. Omit every source for text-only generation. Select
only media you may access; the source's type, metadata, and moderation checks
still apply. For file preparation see [prepare your media](https://developers.apob.ai/guides/preparing-media.md).
MCP's narrower upload objects contain only `url`; do not copy their shape into a
GraphQL `uploadedImage` without its required dimensions.

A video source is inserted as the first ready scene. `initialLength` pays for
newly generated continuation, excluding that source's duration. A source channel
uses its latest usable ready scene, if one exists; otherwise it falls back to
text-only generation. Referencing another channel creates a separate channel.

```graphql theme={"system"}
mutation CreateChannel($input: CreateLiveRoomInput!) {
  createLiveRoom(input: $input) {
    id
    title
    status
    resolution
    isPublic
    audienceSceneCreationEnabled
    cloudflareHlsPlaybackUrl
  }
}
```

```json theme={"system"}
{
  "input": {
    "initialPrompt": "A tiny robot explores a greenhouse on the moon.",
    "initialLength": 15000,
    "resolution": "P480",
    "isPublic": false,
    "audienceSceneCreationEnabled": false
  }
}
```

Save the returned `id`. Channel creation and prepaid generation are asynchronous;
`LIVE` is not a claim that a viewer can already play the stream. The GraphQL
`LiveRoom` selection does not expose `initialLength`; MCP includes it in its
own creation receipt.

## Read playback and scene state

```graphql theme={"system"}
query ChannelPlayback($id: ID!) {
  liveRoom(id: $id) {
    id
    title
    status
    isPublic
    audienceSceneCreationEnabled
    previewImageUrl
    generatedDurationMs
    cloudflareHlsPlaybackUrl
    cloudflareReplayHlsPlaybackUrl
    replayVideoUrl
    startedTime
    endedTime
    scenes {
      isStreamEnded
      candidates { id prompt votedCreditAmount useFreeVote }
      queued { id prompt videoUrl generatedTime }
      current { id prompt videoUrl generatedTime streamStartedTime }
      pushed { id streamStartedTime }
      generated { id videoUrl streamStartedTime }
    }
  }
}
```

```json theme={"system"}
{ "id": "<CHANNEL_ID_FROM_RECEIPT>" }
```

| Field | How to interpret it |
| - | - |
| `status` | Channel lifecycle: `LIVE` or `ENDED`. |
| `cloudflareHlsPlaybackUrl` | Nullable live HLS URL; use the returned URL unchanged, including its low-latency query parameter. Verify readiness through an HLS player's events. |
| `generatedDurationMs` | Actual completed newly generated scene durations in milliseconds; excludes the source video's duration and playback repetitions. |
| `scenes.candidates` | Proposed next scenes and their vote weights. A candidate is not a generation receipt. |
| `scenes.queued` | Selected scenes awaiting generation, currently generating, or ready but not yet pushed. |
| `scenes.current` | Highest-sequence pushed scene, nullable; its runtime start callback may still be unrecorded. |
| `scenes.pushed` | All pushed scenes, including current, in descending sequence order. |
| `scenes.generated` | Pushed scenes excluding current, in descending sequence order. This field does not mean all successfully generated scenes. |
| Scene `generatedTime` | Successful generation completion, Unix epoch milliseconds; nullable. |
| Scene `streamStartedTime` | Earliest recorded runtime start, UTC Unix epoch milliseconds; nullable. Later replays retain the earliest time. |
| `scenes.isStreamEnded` | Queue empty and the latest pushed scene has started at least three times, including replays; separate from channel `ENDED`. |
| `cloudflareReplayHlsPlaybackUrl` | Nullable replay HLS URL. |
| `replayVideoUrl` | Nullable signed MP4 assembled from completed scenes in order, available after postprocessing. Read again for a fresh signed URL. |

Poll the saved channel with spaced reads and a bounded waiting policy. Display a
preparing state while the HLS player waits. A URL, successful generation, or
runtime timestamp alone is insufficient proof that the viewer's player is
playing. Channels can close after inactivity; plan for `ENDED` rather than assuming
an indefinite stream.

## Discover channels and reconcile a lost receipt

```graphql theme={"system"}
query MyChannels($page: PageFetchingInfo!) {
  listPersonalLiveRooms(pageFetchingInfo: $page) {
    list { id title initialPrompt status createdTime cloudflareHlsPlaybackUrl }
    hasMore
  }
}
```

```json theme={"system"}
{ "page": { "pageSize": 10, "pageNumber": 1 } }
```

`listPersonalLiveRooms` requires authentication and returns the caller's channels.
`listPublicLiveRooms` uses the same page input and return shape for public channels.
Page size is `1–100`, default `30`. Use either `pageNumber` (starting at 1) or
`previousId`, not both. For cursor continuation, pass the last returned channel ID
with `pageSize` and omit `pageNumber`. Check `hasMore`.

For an uncertain creation, inspect the same account's channel history by time and
prompt before submitting again. A similar prompt alone is not proof that two
requests are the same operation. Preserve the approximate request time and any
receipt or error for investigation.

## Add scenes and vote

```graphql theme={"system"}
mutation ProposeScene($input: CreateLiveSceneInput!) {
  createLiveScene(input: $input) {
    id roomId prompt votedCreditAmount useFreeVote
  }
}
```

```json theme={"system"}
{
  "input": {
    "roomId": "<CHANNEL_ID_FROM_RECEIPT>",
    "prompt": "The robot plants a glowing seed beside the greenhouse window.",
    "creditAmount": 0
  }
}
```

`roomId`, nonblank `prompt`, and integer `creditAmount` are required. Zero makes
a candidate without an initial vote. Positive amounts charge eligible credits.
Use your own channel or a public channel. It must be live; audience proposals require
the channel's audience policy to permit them. Owner requests funding more than 15 seconds can
create a batch of 5–15 second scenes, at most 1000. The response contains the
first scene; inspect channel scene lists for the rest.

```graphql theme={"system"}
mutation VoteScene($input: VoteLiveSceneInput!) {
  voteLiveScene(input: $input) {
    voteId
    creditAmount
    refunded
    scene { id roomId prompt votedCreditAmount useFreeVote }
  }
}
```

```json theme={"system"}
{
  "input": {
    "roomId": "<CHANNEL_ID_FROM_RECEIPT>",
    "sceneId": "<SCENE_ID_FROM_RECEIPT>",
    "creditAmount": 0
  }
}
```

`roomId`, `sceneId`, and integer `creditAmount` are required. Amounts must be
nonnegative and fit GraphQL `Int` (`0–2147483647`). A zero vote uses the account's
one free vote per scene, adding 100 weight. A positive vote charges that many
eligible credits and adds equal weight. `useFreeVote: true` means this caller
has already used the free vote. `creditAmount` in the vote receipt records what
that vote charged; `votedCreditAmount` is total vote weight, including free votes.

Selection requires enough vote weight to fund at least ten seconds. A candidate
or a free vote does not guarantee selection, generation, or playback. Invalid
channel/scene relationships, ended channels, ineligible scenes, moderation rejection,
and insufficient credits remain errors.

## Owner controls

Only the channel owner can change audience creation, stop, or reopen. Run each
mutation separately with the appropriate variables.

```graphql theme={"system"}
mutation SetAudience($id: ID!, $enabled: Boolean!) {
  setLiveRoomAudienceSceneCreationEnabled(id: $id, enabled: $enabled) {
    id audienceSceneCreationEnabled
  }
}
```

```json theme={"system"}
{ "id": "<CHANNEL_ID_FROM_RECEIPT>", "enabled": true }
```

```graphql theme={"system"}
mutation StopChannel($id: ID!) {
  stopLiveRoom(id: $id) { id status endedTime }
}
```

```json theme={"system"}
{ "id": "<CHANNEL_ID_FROM_RECEIPT>" }
```

Stopping an already ended owned channel returns that channel. Observe replay fields
after cleanup and postprocessing; a stop receipt does not promise an immediate
download.

```graphql theme={"system"}
mutation ReopenChannel($id: ID!, $initialLength: Long!, $initialPrompt: String) {
  reopenLiveRoom(id: $id, initialLength: $initialLength, initialPrompt: $initialPrompt) {
    id status resolution isPublic audienceSceneCreationEnabled cloudflareHlsPlaybackUrl
  }
}
```

```json theme={"system"}
{ "id": "<ENDED_CHANNEL_ID>", "initialLength": 15000 }
```

Reopening requires an owned ended channel. It creates a **new channel ID** and prepays
new generation, using the old channel as source and inheriting its resolution,
visibility, and audience policy. Omit `initialPrompt` to reuse the old prompt.
Store the new ID; the previous channel remains ended.

## Credits and duration planning

Read current configured rates rather than hard-coding prices:

```graphql theme={"system"}
query LiveRates {
  getTaskCostList { type subtype cost unitAmount }
}
```

Use the entries for `live480PVideoContentGeneration` and
`live768PVideoContentGeneration`. For Live, `unitAmount` is milliseconds.
Initial generation splits the normalized duration into segments of at most
15 seconds, with a minimum five-second tail. Each segment costs
`ceil(segmentDurationMs / unitAmount) * cost` credits under its configured rate.
For example, `initialLength: 16000` plans a 15-second segment plus a five-second
segment. The duration requested is not an exact price or output-duration guarantee.

Initial generation uses the rate's configured eligible credit categories.
Positive scene creation and paid voting use `common` (general-purpose) credits.
A total balance can include ineligible credits. Automatic reload can have separate
payment effects according to the account and connection's policy; inspect
[credits and billing](https://developers.apob.ai/credits-and-billing.md).

## Handle errors without duplicating work

Read GraphQL `errors` even on HTTP 200, and retain any returned channel/scene IDs
when the response is partial. A timeout, field error, or lost response can occur
after a write was accepted. Create, reopen, scene, and paid-vote operations can
spend credits again if repeated. There is no create idempotency key in these
public operations.

Reconcile existing channels, scenes, votes, and account activity before considering
another write. Retry reads using the same ID. Do not bypass access, moderation,
audience, lifecycle, or credit rejection by switching from API to MCP. See
[Live troubleshooting](https://developers.apob.ai/troubleshooting.md#the-channel-is-live-but-playback-has-not-started).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.