Skip to main content

Video room features: chat, polls, Q&A, invites, RTMP

Once everyone is in and media is healthy, failures move from the lifecycle (mint, capacity, lobby) to the in-room features: a chat message that will not send or gets blocked by the content policy, a poll that refuses to launch, an invite that a guest cannot redeem, a lock or ban the host cannot apply, a whiteboard stroke that desyncs, or an RTMP push that starts but the playback URL never resolves. Every one of those is a refusal with an error.code on the API response — match the code to the feature section below and work it. Lifecycle refusals (join token, capacity, lobby, dispatch) live on Troubleshooting: video room lifecycle failures; quality symptoms after a successful join live on Troubleshooting: video room and call quality. This page covers the feature-level VIDEO_* codes those pages point past. Most feature codes are safe to retry after the config hint is fixed — each feature runs as a server-side broadcast or audit-backed write, so a retry never double-applies. The gateway codes (VIDEO_CHAT_BLOCKED_BY_POLICY, VIDEO_INVITE_REDEEM_FAILED, VIDEO_ROOM_LOCKED) are the exception: a retry with the same content or token refuses the same way.

Chat

The in-room text chat routes through a per-room content policy before the broadcast, so most chat failures are policy verdicts, not transients.

VIDEO_CHAT_SEND_FAILED

The broadcast to the room’s data channel failed after the policy screen passed. Transient — retry with backoff; the envelope carries the client-supplied message_id, so a retry does not duplicate the message in replay history.

VIDEO_CHAT_BLOCKED_BY_POLICY (422)

The room’s content policy rejected the message — it is never broadcast, never persisted, and never enters the replay history or recording. The response carries error.details.categories naming the policy category that matched (or a blocklist hit). Do not retry the same text: either edit the message content, or adjust the room policy on PUT /:id/chat/policy (block / flag / log modes) if the category is configured too strictly. flag and log modes deliver the message; only block refuses.

VIDEO_CHAT_POLICY_UNAVAILABLE (503)

The policy read failed, and the route fails closed rather than deliver unchecked content. Retry once; if it repeats across a few minutes, the policy evaluation layer is degraded — escalate with the request_id.

VIDEO_CHAT_DELETE_FAILED

Host moderation — removing a single message from the shared history — failed. Retry once after fetching fresh chat; another host may have already removed it.

VIDEO_CHAT_CLEAR_FAILED

Host cleared the entire deck failed. Same handling — check fresh state before a second clear so you do not clear messages added between the two attempts.

Polls & Q&A

Polls and moderated Q&A run as server-side broadcast envelopes over the data channel, so failures are almost always transient data-plane refusals.

VIDEO_POLL_LAUNCH_FAILED

Only the room’s host, panelist, or moderator session can launch — a viewer role earns a 403 from the write guard before this code ever fires. The launch itself failed: retry with backoff; the poll_id you choose is client-generated, so a retry replaces the same poll, never duplicates it.

VIDEO_POLL_VOTE_FAILED

The vote write failed. The voter_identity is server-stamped (a client cannot vote as someone else), so a retry is safe — retry with the same poll_id and option_ids.

VIDEO_POLL_CLOSE_FAILED

Host-side close failed. Fetch the current poll state first; a close that partially applied is treated as open for a retry.

VIDEO_QA_ASK_FAILED

Question submission failed. Retry with the same question_id — client-chosen, so no duplicate question lands on a retry.

VIDEO_QA_ANSWER_FAILED

Host answer post failed. Retry once; check fresh Q&A state first in case the answer landed and the response dropped.

Invites

Guest-invite tokens are minted per room by the host, then a guest redeems the link. Most invite failures are lifecycle stages of the token, not transients.

VIDEO_INVITE_CREATE_FAILED

Token mint failed downstream of validation. Retry with backoff — expires_in_hours (1–168 hours, default bounded) and max_uses (1–500) bound the token’s lifetime, so a retry only issues another bounded token.

VIDEO_INVITE_REDEEM_FAILED

The guest clicked /video/invites/:inviteToken/redeem and the token either expired, exhausted max_uses, or was revoked. The refusal message says the link may have expired, but any of those three apply. The fix is never a retry: issue a fresh invite and hand the new link to the guest. On a multi-use invite, check GET /:id/invites for the token’s remaining uses.

VIDEO_INVITE_REVOKE_FAILED

Host revoke failed. List the invites first — a stale invite_id or a racing revoke by another host already removed it. Retry only after a fresh list.

VIDEO_INVITE_LIST_FAILED

Listing the room’s invites failed while computing the active-token field. Safe to retry — read-only; if the 500 repeats across minutes, escalate with request_id.

Moderation

Moderation actions are host/panelist-only writes, split across the participant-ban list, role/tier handoff, and the room lock.

VIDEO_BAN_LIST_FAILED

Loading the persisted banned-participants list (GET /:id/bans) failed. Read-only; retry.

VIDEO_PARTICIPANT_BAN_FAILED

Ban write failed (POST /:id/participants/:identity/ban). Retry after fetching fresh room state — the ban row is idempotent on the identity, so a retry that lands after a racing unban simply re-bans.

VIDEO_PARTICIPANT_UNBAN_FAILED

Ban-lift failed (DELETE /:id/participants/:identity/ban). Check the ban list first; a missing row means another host already lifted it.

VIDEO_PARTICIPANT_ROLE_UPDATE_FAILED

Role handoff (host / panelist / viewer) failed. Only an owner/admin session can re-tier into host; the route refuses below that with a 403 before this 500 fires. Retry after re-fetching the participant’s current role.

VIDEO_PARTICIPANT_TIER_UPDATE_FAILED

Publish/receive tier update failed (POST /:id/participants/:identity/tier). The tier names map onto the publish/receive grant; retry after re-fetching the participant.

VIDEO_PARTICIPANT_RECEIVE_CONTROL_FAILED

Per-subscriber receive-settings update failed (POST /:id/participants/:identity/receive-control). Transient — retry.

VIDEO_ROOM_LOCKED (409)

POST /:id/lock claims the room is currently locked so the action the caller attempted (often a join, or a lock-opposite-precondition write) was refused. Read the room state first — a locked room refuses joins by design. Unlock, then retry the original action.

VIDEO_ROOM_LOCK_FAILED

Lock/unlock toggle failed mid-transition. Fetch the current lock state before a second toggle so you know which direction to flip.

Hands, spotlight, whiteboard

These are the in-room engagement + moderator-pin surfaces — all broadcast envelopes over the data channel, so the failure shape matches polls.

VIDEO_HAND_RAISE_FAILED

Raise-hand failed (POST /:id/hand-raise). Only owners and admins can raise on behalf of another identity below this update — a role-gate 403 fires before the 500. Retry.

VIDEO_HAND_LOWER_FAILED

Lower-hand or queue-clear failed (POST /:id/hand-lower, or host POST /:id/hand-queue/clear). Fetch the raised-hand queue (GET /:id/hand-queue) first, then lower only identities still on it.

VIDEO_SPOTLIGHT_SET_FAILED

Spot/pin a participant failed (POST /:id/spotlight). Only one spotlight is live at a time — a second set clears the first — so retry after checking which participant is pinned.

VIDEO_WHITEBOARD_OPEN_FAILED / VIDEO_WHITEBOARD_DRAW_FAILED / VIDEO_WHITEBOARD_CLOSE_FAILED / VIDEO_WHITEBOARD_CLEAR_FAILED

Whiteboard lifecycle (/open, /draw, /close, /clear). The whiteboard is a shared broadcast surface; the open sets the scene, one stroke per draw envelope, and close/clear tear it down. If DRAW_FAILED repeats while open was clean, the room may have rotated its channel — re-open before retrying the stroke. CLEAR_FAILED and CLOSE_FAILED both route the host, not a guest — a 403 from the guard precedes them.

Livestream & RTMP

RTMP egress and HLS/WHEP playback run through the egress control plane.

VIDEO_RTMP_EGRESS_START_FAILED

The composite render start refused (POST /:id/egress/rtmp). Validate the body first — the most common cause is a malformed destination (the rtmp:// scheme on rtmp_url, stream_key as the separate field). The control plane echoes URLs without keys and never persists the stream key, so a retry that fails the same way will not leak it. When the body is clean, retry once before escalating.

VIDEO_RTMP_EGRESS_STOP_FAILED

DELETE /:id/egress/rtmp/:egressId is idempotent — retry the delete; a leaked egress keeps rendering an empty room to a live destination, so stop it even if the room endocard finished. If the stop persists through two retries, escalate with egress_id.

VIDEO_LIVESTREAM_PLAYBACK_FAILED

The playback URL for the fan-out (GET /:id/livestream, or the DVR timeshift route) failed to resolve after egress start. Retry once; if it repeatedly refuses, check whether the egress is still running — a stopped egress cannot mint a playback URL.

SSO policy

Rooms can clamp access to your enterprise IdP with a per-room SSO required toggle.

VIDEO_ROOM_SSO_POLICY_FAILED

Both the GET and POST /:id/sso-policy surfaces carry this code because the same policy evaluation runs on both paths. The refusal is not a misconfiguration message — it says the policy read or update failed. Key behavior: the room derives can_arm on the GET by evaluating the ARMED candidate policy, so a session that is not currently SSO-federated reports can_arm: false — and the toggle stays clickable into the guaranteed 409 below. VIDEO_ROOM_SSO_POLICY_FAILED on POST almost always follows the self-lockout guard: a session that would be locked out by enabling required: true refuses the update. Federate your session through SSO or drop the requirement before posting; the GET above tells you whether the current session could survive the armed policy.

When to escalate

Work the feature section first — most codes resolve on the first config fix or one retry. When the same code survives about ten minutes of retries, open a ticket with:
  1. Room id and, when you have it, the room_sid from the join response.
  2. Error code verbatimerror.code from the response body.
  3. Feature endpoint and request body — e.g. POST /:id/polls/vote with poll_id.
  4. request_id — the meta.request_id on the response envelope.
  5. Timestamp in UTC of the refusal.

See also