> ## 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.

# Troubleshooting: video room lifecycle failures — joins, capacity, lobby, agent dispatch, and RTMP egress

> Diagnose video rooms that never start or refuse a specific caller: join-token mint refusals, participant-cap 400s, lazy-open failures on scheduled rooms, lobby toggle and admit failures, AGENT_NOT_DEPLOYABLE on dispatch, and RTMP egress errors.

# Troubleshooting: video room lifecycle failures — joins, capacity, lobby, agent dispatch, and RTMP egress

A lifecycle failure is different from a bad room: the guest never got a
token, the API refused the create with a 4xx, a scheduled room failed to
open on first join, the lobby would not toggle or admit, the agent
dispatch was rejected, or the RTMP push never started. These are
refusals with an error code on the response — read the code, work the
section for it, and the room either opens or tells you exactly what to
change.

Quality symptoms after everyone is in — frozen video, echo, a degraded
recording verdict, a failed PSTN dial-out — live on
[Troubleshooting: video room and call quality](/troubleshooting/video-call-quality).
This page covers the operational lifecycle only: mint, open, capacity,
lobby, dispatch, and egress.

## Where the failure surfaced

The same refusal looks different depending on which surface it hit —
find your surface first, then match the error code.

| Surface                                                                  | What you see                                                                                                                        | First check                                                                                                                                                                                 |
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **API response**                                                         | A non-2xx body with `error.code` on `POST /rooms-scheduled`, `/:id/join`, `/:id/dispatch-agent`, `/waiting-room`, or `/egress/rtmp` | Match `error.code` against the sections below — every lifecycle refusal is named there                                                                                                      |
| **Dashboard**                                                            | A room fails to open, a join spins and dies, or a dispatch button errors                                                            | Re-run the same flow from the API once; the dashboard surfaces the same codes, and the response envelope carries the `request_id` support needs                                             |
| **Embed** (`orbit-video-room` element, or the dashboard's own join flow) | The element errors or a guest is stuck on the join screen                                                                           | Your back end mints the token for the element — inspect the join response your server received, then read the returned `permissions` and `participant_tier` before assuming a media problem |

## Join-token failures

Joining is a two-step wire: your back end mints a per-participant token
on `POST /api/v1/video/rooms-scheduled/:id/join`, and the browser trades
the token for the SFU websocket. Lifecycle failures live in step one —
the mint — and four codes cover them.

### `VIDEO_ROOM_JOIN_FAILED` (500)

Identity, grant, and tier checks all passed, but the mint itself failed
downstream when the platform signed the access JWT or reached the media
layer (`Orbit Media`). The request did not complete, so no token was
issued and nothing joined.

`VIDEO_ROOM_JOIN_FAILED` is transient-frequent: retry with backoff.
Because every mint writes a fresh, bounded token — never a partially
valid one — a retry leaves no state to clean up. Read `uses` before you
loop: a default lifetime is 1 hour (override with `ttl_seconds` on the
body), so a mint-and-cache client that retries a stale cached token is
debugging its own cache, not the mint — mint fresh when `expires_at`
approaches, replace the token in the client, then retry.

If the 500 repeats within **ten minutes** of retries, stop looping and
escalate with the `request_id` from the response `meta` — a persistent
mint failure is platform-side.

### `IMPERSONATION_NOT_PERMITTED` (403)

You sent an `identity` that is not your own user id, and the caller's
tenant role is not `owner` or `admin`. The identity gate exists because
any caller who could mint under a teammate's identity could join-as-them
and publish under their name.

The retry-safe shape is one of two, never a role workaround:

1. Drop `identity` from the body — the mint defaults it to the
   authenticated caller, and the common self-join
   flow ships unchanged.
2. Use an owner or admin key when you genuinely must mint-as-another
   (e.g. minting guest tokens server-side for your own users), and keep
   the `impersonation_reason` (below) — denied attempts write an audit
   row, so a loop of 403s from a developer-role key is an alertable
   abuse signal, not just a refusal.

### `IMPERSONATION_REASON_REQUIRED` (400)

An owner or admin minted under another user's identity but sent an empty
or missing `impersonation_reason`. Supply a non-empty, non-whitespace
reason string on the same request — it lands on the mint audit row so
your compliance team can answer "who minted a token under Alice at
14:32" without correlating logs. Retrying with the reason is the fix;
no room state was touched by the refusal.

### `PARTICIPANT_TIER_NOT_PERMITTED` (403)

You requested `participant_tier` of `host` or `hidden_supervisor` with a
caller role below `owner`/`admin`. Those tiers carry publish and (for
`host`) room-admin grants, so a viewer or developer session cannot
bootstrap them by posting the field. Fix by dropping the requested tier
(let the service derive `panelist`/`viewer` from the room), or mint from
an owner/admin key. A refused mint and a lobby downgrade are different
things: when an armed waiting room holds a join, the response reports
`lobby_downgraded: true` with receive-only permissions — that is the
lobby doing its job, not a tier refusal. Always read the returned
`participant_tier` and `permissions`, not the tier you asked for.

## Capacity and open failures

### `VIDEO_PARTICIPANTS_CAP_EXCEEDED` (400)

`max_participants` on create (or on a later patch) exceeded your
tenant's resolved cap — currently the 300-participant ceiling on the
self-serve plan, before any operator override set on your own
organization's settings. The response `error.details` carries three
fields that name the fix directly: `requested` (what you sent), `cap`
(the resolved ceiling), and `tier`. Two tenant-owned paths, in order:

1. Lower the room — send `max_participants` ≤ the returned `cap`, or
   omit it to inherit the resolved cap. Every default already inherits
   correctly, so this refusal only follows an explicit request that
   overshot the ceiling.
2. Raise the ceiling — when a genuinely larger room is needed, open a
   support ticket (see below) with the `requested`/`cap`/`tier`
   triple from `details`; that triple is exactly what support needs to
   evaluate an override on your organization.

Don't retry the unchanged body: the request is validated before any
room state is written, so a fourth-hundred loop creates nothing.

### `VIDEO_ROOM_CREATE_FAILED` / `VIDEO_ROOM_OPEN_FAILED`

Both mean a reachability or configuration failure on the media layer at
the moment Orbit tried to stand the room up, with different timing:

* `VIDEO_ROOM_CREATE_FAILED` — thrown during room creation, before any
  room record settles. A retry is a fresh create; the failed attempt
  left nothing behind.
* `VIDEO_ROOM_OPEN_FAILED` — thrown at lazy-open: a scheduled room with
  a future `scheduled_at` does not open until the first host or
  participant actually joins. The failure then surfaces on the join
  request, not on the create. The room record is still valid — retry
  the join rather than recreating the scheduled room; a re-created room
  duplicates the record and inherits the same media condition.

Retry both with backoff; a refusal that survives ten minutes of retries
is platform-side — escalate with `request_id`.

## Waiting-room failures

Two codes cover the lobby, both transient-frequent and both safe to
retry:

* `VIDEO_WAITING_ROOM_FAILED` — thrown when toggling the lobby on
  `POST /api/v1/video/rooms-scheduled/:id/waiting-room`. Retry the
  toggle; the lobby state is left either on or off, never half-armed,
  so a retry never double-arms.
* `VIDEO_WAITING_ROOM_ADMIT_FAILED` — thrown when admitting a pending
  participant on `POST /:id/waiting-room/admit/:identity`. Retry the
  admit. There is a per-participant subtlety: if the identity you sent
  never landed in the lobby (typoed `identity`, or the guest re-joined
  through the lobby again), clean the roster with a fresh fetch of the
  room rather than re-admitting.

If your flow holds participants open with `auto_promote: true`, remember
the hold still applies to the join — only the first host entry lifts
everyone. An admit error while no host has entered is the lobby working
as configured; dispatch or join a host first.

## `AGENT_NOT_DEPLOYABLE` on dispatch-agent

`POST /api/v1/video/rooms/:id/dispatch-agent` refuses with a 409 when
the referenced agent is not in the `active` state — the platform will
not put an agent that is not running into a live room as a participant.
The refusal fires before any media call: no join was attempted, the
room is untouched, and the agent was never a participant. A draft,
paused, or archived agent all refuse the same way. Two checks
in order:

1. Fetch the agent and read its `status`. If it is anything but
   `active`, resume or activate it from the dashboard or the agents
   API, then dispatch once.
2. `AGENT_NOT_FOUND` (404) is the sibling refusal — the agent id does
   not resolve in your tenant schema. Re-list your agents and take a
   fresh id; a tenant-id guess against another tenant's agent is the
   common cause.

Dispatch is owner/admin plus `video:write` only — a 401/403 there is a
credentials problem, not the 409 gate.

### The agent state map

The agent's `status` field moves through four states, and the 409
names which leg you are on:

| Status     | How it got there                                                                                                                           | What re-arms it to `active`                                                                          |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `draft`    | Newly created, or duplicated from another agent (a duplicate always lands here)                                                            | Publish it — `POST /api/v1/agents/:id/deploy` with a channel, or the dashboard's **Activate** action |
| `active`   | A successful deploy                                                                                                                        | — nothing to do; re-dispatch                                                                         |
| `paused`   | Undeployed (`POST /:id/undeploy`), or deactivated from the dashboard — the deactivate path always lands on `paused`, never back on `draft` | Redeploy — `POST /:id/deploy` flips `paused` back to `active` regardless of prior state              |
| `archived` | Deleted/retired from the dashboard or the agents API                                                                                       | Unarchive the agent, then deploy it again                                                            |

The common 409 sequence is: created the agent, never deployed it, then
dispatched straight from a job or dialer flow. The dispatch endpoint
never promotes the agent for you — activation is an explicit tenant
step, so the workflow that dispatches must also own the deploy when
the status read comes back `draft`.

### Related errors on the same pipeline

These surface from the same room while an assignment is live, but they
are distinct from the dispatch gate:

* `VIDEO_PARTICIPANT_KICK_FAILED` — the `removeParticipant` media call
  failed while kicking a participant. Transient-frequent; retry once
  after fetching fresh room state (the participant may already be out).
* `VIDEO_PARTICIPANT_MUTE_FAILED` — the `mutePublishedTrack` media
  call failed while muting a participant's track. Same handling.
* `VIDEO_RECORDING_STOP_BACKEND_FAILED` — the `stopEgress` call
  rejected while stopping a room recording. Retry the stop once; a
  recording that keeps running bills room time, so escalate with the
  room id if a second stop fails.
* `RECORDING_SIGN_FAILED` (503) — minting the signed playback URL for
  a recording failed at the storage layer. A retry after a brief
  backoff resolves most occurrences; a persistent one is
  platform-side.

`VIDEO_WEBHOOK_VERIFICATION_FAILED` is **not tenant-actionable**: it
means an Orbit Media webhook reached Orbit with a JWT signature that
could not be verified (clock skew, a mis-rotated secret, or forged
traffic). Orbit monitors it platform-side — do not chase it in your
own webhook handler; report it if it correlates with missing
recording/lifecycle event deliveries.

### Dispatch escalation bundle

When the refusal or the follow-on failures survive the section above,
open a ticket with:

1. **Room id** and, when you have it, the **`room_sid`** from the join
   response.
2. **Agent id** and the agent's **`status` at the time of dispatch**
   (`GET` the agent alongside the refusal).
3. **Error code verbatim** — `AGENT_NOT_DEPLOYABLE`, or the
   participant/recording code that followed it.
4. **`request_id`** — the `meta.request_id` on the response envelope.
5. **Timestamp in UTC** of the refusal.

## RTMP egress errors

`POST /api/v1/video/rooms-scheduled/:id/egress/rtmp` surfaces two
failure shapes, both actionable from the body you sent:

* **`SERVICE_UNAVAILABLE`** (503) — live streaming was requested when
  Orbit Media is not configured on this deployment. No retry changes
  it; the egress control plane is absent, not failing. Escalate if you
  expected it to be configured.
* **Destination refusals** — the response echoes URLs without keys
  (`egress_id, rtmp_url, additional_rtmp_urls`), so compare the outset
  of your `rtmp_url`/`stream_key` pair against the platform the stream
  targets. The most common refusal is a malformed destination —
  swap the URL and the key (`rtmp://` scheme on the URL, secret-bearing
  stream key as the separate field), or a backup/additional destination
  carrying a primary's URL.

Fan-out limits: additional destinations render once and fan out, so one
simulcast to YouTube plus Twitch costs a single composite — but you
still need to stop the fan-out with
`DELETE /:id/egress/rtmp/:egressId` when the stream ends. A leaked
egress keeps rendering an empty room to a live destination, which is a
publish failure shape of its own.

## When to escalate — the support bundle

Work the section first; when the same error code survives ten minutes
of retries, open a ticket with:

1. **Room id** — from the create/join response or any `video.*`
   webhook, plus the `room_sid` off the join response when you have
   one.
2. **Error code verbatim** — `error.code` from the response body.
3. **`request_id`** — the `meta.request_id` on the response envelope.
4. **Tenant id** (your organization id — Settings → Organization).
5. **Timestamp in UTC** — when the refusal hit, so support can pull
   the exact window.
6. For dispatch failures: the **agent id** and the agent's **status at
   the time of dispatch** (`GET` the agent alongside the refusal).

## See also

* [Video channel: rooms, embeds, recording, and broadcast](/channels/video)
  — join-token grants and tiers, the lobby, capacity limits, egress,
  and the errors table this page works from.
* [Error codes reference](/reference/error-codes) — the full video-room
  and waiting-room code families.
* [Agents API reference](/api-reference/agents) — Deploy Agent and
  Undeploy Agent, the re-arm endpoints the state map above works.
* [Video API reference](/api-reference/video) — the request/response
  contract for rooms, join, waiting-room, dispatch-agent, and egress.
* [Troubleshooting: video room and call quality](/troubleshooting/video-call-quality)
  — the quality sibling for ICE, frozen tiles, echo, degraded-recording
  QC, and PSTN dial-out after a successful join.
* [Glossary](/reference/glossary) — grant tier, lobby, and SFU terms
  this page uses.
