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

# Video

## Worked room lifecycle: create → token → record → fetch

The run-through every video integration does first: create a room, mint a
join token for a participant, record the session, then read the session's
signed playback URL once the room has ended. All requests use your API key
(`X-API-Key`) against `https://api.orbit.devotel.io`.

### 1. Create the room

<Note>
  `POST /api/v1/video/rooms`
</Note>

Room names must be URL-safe (letters, digits, `_`, `-`, 3–200 characters).
The response returns the SFU room name plus a host token and the WebSocket
URL the host client connects to.

**Request**

```json theme={null}
{
  "name": "weekly-standup",
  "max_participants": 25
}
```

<ResponseExample>
  ```json 201 theme={null}
  {
    "data": {
      "room_name": "tenant_1a2b3c4d_5e6f_7081_weekly-standup",
      "display_name": "weekly-standup",
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.host.jwt",
      "url": "wss://media.orbit.devotel.io"
    },
    "meta": {
      "request_id": "req_create_room",
      "timestamp": "2026-08-24T09:15:00.000Z"
    }
  }
  ```
</ResponseExample>

`room_name` carries a server-side tenant prefix; `display_name` is the
label you supplied. Feed the returned `token` and `url` to a LiveKit client
SDK to pick up audio and video — `token` is a LiveKit access JWT, not an
HTTP bearer token.

The scheduled-room surface (recurrence, registration, simulive, webinar
layouts) is a sibling surface under `POST /api/v1/video/rooms-scheduled` —
its create call also mints the orbit-media egress when
`recording_enabled: true`, so skip step 3 for those rooms.

### 2. Mint a participant access token

<Note>
  `POST /api/v1/video/rooms/{name}/token`
</Note>

One call per participant. `participant_name` is the identity stamped on
the token; `can_publish` defaults to `false` (receive-only) and only an
owner or admin caller can set it to `true`. Minting a token under someone
else's identity additionally requires `impersonation_reason` and an owner
or admin role, or the request is rejected with a four-hundred status.

**Request**

```json theme={null}
{
  "participant_name": "ada@example.com",
  "can_publish": true,
  "participant_tier": "panelist"
}
```

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.participant.jwt",
      "url": "wss://media.orbit.devotel.io"
    },
    "meta": {
      "request_id": "req_mint_token",
      "timestamp": "2026-08-24T09:16:00.000Z"
    }
  }
  ```
</ResponseExample>

`participant_tier` is the finer-grained control: `viewer` (receive-only),
`panelist` (publishes audio/video), `host`, and `hidden_supervisor`. When
supplied it overrides `can_publish`; `host` and `hidden_supervisor` require
the owner or admin role. When omitted, the legacy `can_publish` boolean
controls the grant exactly as before.

### 3. Record the session

Recording is started by the host from inside the room (or automatically at
room creation for scheduled rooms created with `recording_enabled: true`),
and completed recordings are attached to the room's session history by the
recording webhook. There is no second API call to make here — once the room
ends, the recording finalizes asynchronously. Listen for the
`video.recording.completed` webhook event (see
[Webhook events](/reference/webhook-events)) or poll `GET /api/v1/video/sessions`
until the session row appears with a non-null `recording_url`.

### 4. Fetch the recording asset

<Note>
  `GET /api/v1/video/sessions/{id}`
</Note>

Each ended room produces one session row. Fetch it by id — the response
carries `recording_url` as a time-limited signed HTTPS URL ready for a
`<video>` element or download, plus `recording_url_expires_at` so you know
when to re-fetch. A session whose recording has not finalized yet returns
`recording_url: null`.

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "vs_2b3c4d5e6f70819a",
      "room_id": "vr_1a2b3c4d5e6f7081",
      "room_name": "weekly-standup",
      "started_at": "2026-08-24T09:16:12.000Z",
      "ended_at": "2026-08-24T09:44:51.000Z",
      "duration_seconds": 1719,
      "max_participants": 25,
      "total_join_events": 7,
      "recording_url": "https://storage.googleapis.com/orbit-recordings-eu/recordings/tenant_1a2b3c4d/vs_2b3c4d5e6f70819a.mp4?X-Goog-Signature=…",
      "recording_url_expires_at": 1787565900000,
      "recording_id": "rec_9f8e7d6c5b4a3162",
      "metadata": {},
      "created_at": "2026-08-24T09:16:12.000Z",
      "updated_at": "2026-08-24T09:45:02.000Z"
    },
    "meta": {
      "request_id": "req_get_session",
      "timestamp": "2026-08-24T09:46:00.000Z"
    }
  }
  ```
</ResponseExample>

`recording_id` is the unified-recordings primary key — use it with the
Recordings API to mint a time-limited guest share link for a non-account
viewer, or to pull the stored transcript. Treat both `recording_url` and
its signature as opaque and re-fetch the session when
`recording_url_expires_at` passes rather than re-signing it yourself.

## Response envelopes

Every endpoint on this page answers in one of two envelope shapes, so one
error handler covers all four steps above.

**Success** — `data` carries the payload (a single object, or an array on
list endpoints such as `GET /api/v1/video/sessions`), and `meta` carries
`request_id` plus `timestamp`. List endpoints add a `pagination` block
under `meta`:

```json theme={null}
{
  "meta": {
    "request_id": "req_list_sessions",
    "timestamp": "2026-08-24T09:46:00.000Z",
    "pagination": {
      "cursor": "eyJs…",
      "has_more": true,
      "total": null
    }
  }
}
```

While `meta.pagination.has_more` is true, pass `meta.pagination.cursor`
back as the `cursor` query parameter to fetch the next page — the cursor is
opaque; keep the same filters across pages and stop when `has_more` comes
back false.

**Error** — failures answer with `{ error, meta }`, where `error` carries
`code`, `status`, and `message`. Validation failures return `422`; token
privilege violations return `403` in this same shape:

```json 403 theme={null}
{
  "error": {
    "code": "PARTICIPANT_TIER_NOT_PERMITTED",
    "status": 403,
    "message": "Minting a participant token with this tier requires the owner or admin role."
  },
  "meta": {
    "request_id": "req_mint_token",
    "timestamp": "2026-08-24T09:16:00.000Z"
  }
}
```

Handle `PARTICIPANT_TIER_NOT_PERMITTED` and `IMPERSONATION_NOT_PERMITTED`
before the generic error path — both mean the caller's role cannot mint the
requested token, and retrying without changing the request will fail the
same way.
