Troubleshooting: video guest chat refusals — VIDEO_CHAT_GUEST_FORBIDDEN, VIDEO_CHAT_LOBBY_HELD, and VIDEO_CHAT_LOBBY_CHECK_UNAVAILABLE
A guest chat refusal is different from a chat moderation block: the message never reached the room at all. The guest posted from the join page — no signed-in user, no tenant context — and the platform refused the send with a 403 or 503 before broadcasting. Every refusal on this route is deliberate and fail-closed: the platform rejects rather than deliver a message it cannot scope to a tenant, attribute to a sender, or screen against the room’s content policy. Nothing is silently re-queued — a refused message is gone, and the recovery is always an explicit re-send after the gate that fired is cleared. This page covers the three refusal surfaces onPOST /api/v1/video/rooms/:id/chat for anonymous guests. A 422 VIDEO_CHAT_BLOCKED_BY_POLICY block — the message was scoped, screened, and
rejected by the room’s content rules — is a different failure family covered
on
Video room features: chat, polls, Q&A, invites, RTMP.
What “send a chat message as a guest” means
A guest is a participant with no Orbit account session — they clicked an invite join link, landed on the guest-join page, and got a LiveKit session token for the room. When they post a chat message, the join page callsPOST /api/v1/video/rooms/:id/chat with two tokens on the body:
join_token— the invite token from the join link. This is how the platform proves the caller was invited to this room (the room UUID alone is guessable, so a send without it is rejected outright).livekit_token— the LiveKit access token the guest connected with. This is how the platform proves this specific guest is present in the room and has been admitted past the waiting room.
The three gates, and why each fails closed
Gate 1 — tenant resolution: 403 VIDEO_CHAT_GUEST_FORBIDDEN
What fired: the caller carried no session tenant, and the invite-token
lookup matched the send to no room record — or no join_token was sent at
all.
Why the gate exists: without a resolved tenant the platform cannot scope
the message to your organization’s data, cannot attribute a sender, and
cannot read the room’s content policy. Delivering anyway would post an
unscreened, unscoped message into a live room — so the platform rejects
instead, every time.
Common causes:
- The guest is chatting with a stale or hand-edited join link — the invite token on the page no longer matches a live invite for the room.
- The invite was revoked or expired after the guest’s page loaded.
- A custom integration posts the chat route directly but only sends the
LiveKit token, not the
join_tokenfrom the invite link — the route cannot scope a send from the room UUID alone.
- Have the guest re-join from a fresh join link — reissue the invite (or ask the host to) so the page loads with a current invite token.
- If you call the route directly, send both
join_tokenandlivekit_token; the join token is not optional for anonymous sends. - Do not retry the unchanged send — a token the lookup cannot match will never match on retry.
Gate 2 — lobby admission: 403 VIDEO_CHAT_LOBBY_HELD
What fired: the room has the waiting room armed, and this guest is still
in the lobby — or isn’t connected to the room at all. The platform verified
the guest’s livekit_token, looked up their live participant state on the
media layer, and found them not yet admitted.
Why the gate exists: a guest still in the waiting room has not been
admitted into the meeting; letting them post into the admitted room’s chat
would punch a hole in the lobby the host armed. The live permission on the
media layer is the source of truth — an admit flips it in place without
issuing a new token, so the guest’s token is checked but their current
state is what counts.
Common causes:
- The guest joined while the lobby was armed and the host hasn’t admitted them yet — the expected state, not an error.
- The guest connected, disconnected, and is re-posting from a cached page without a live session in the room.
- Ask the host to admit the guest from the waiting room
(
POST /api/v1/video/rooms-scheduled/:id/waiting-room/admit/:identity, or the admit control in the dashboard lobby panel). - The guest then re-sends the message — after admission the same
livekit_tokenpasses the gate, because the live permission moved, not the token. - If the guest is no longer connected at all, have them re-join from the invite link first.
403 VIDEO_CHAT_GUEST_FORBIDDEN at the same gate: the token no longer verifies,
is scoped to a different room, or the request carried no livekit_token.
The response message tells the guest to refresh and rejoin from the invite
link — that is the fix, not a retry of the same tokens.
Gate 3 — the lobby check itself degraded: 503 VIDEO_CHAT_LOBBY_CHECK_UNAVAILABLE
What fired: the room has the waiting room armed and the platform could
not run the lobby assertion — the media-layer lookup or tenancy resolution
errored. The guest’s admission state is unknown.
Why the gate fails closed: an unknown lobby state is treated as
not-admitted, not as admitted. Confirming a guest is in the room is a
security check; when the check cannot complete, the message is not
delivered. This is deliberate — the safe side of an outage is a refused
chat, not an unscreened one.
Fix: retry the send with backoff — this refusal is transient-frequent
(a media-node blip or a connection storm). If 503s persist for more than a
few minutes, the media layer is degraded; escalate with the bundle below
rather than continuing to loop.
The policy-read sibling: 503 VIDEO_CHAT_POLICY_UNAVAILABLE
One more fail-closed path rides the same route: after the sender clears
both gates above, the platform reads the room’s chat content policy before
broadcasting. If that policy read throws, the send is refused with
503 VIDEO_CHAT_POLICY_UNAVAILABLE — same never-deliver guarantee, same
retry-with-backoff handling. A policy the platform cannot read cannot be
honored, so the message is rejected rather than posted unscreened.
Worked example — a guest send refused, then recovered
The guest posts from the join page; the request the page issues is:- The host admits the guest
(
POST /api/v1/video/rooms-scheduled/9b1d2c3e-…/waiting-room/admit/guest-7f2aor the dashboard Admit button). - The guest re-sends the same message — no new tokens needed; the admit changed their live permission.
- The second
POSTreturns200and the message broadcasts into the room.
403 VIDEO_CHAT_GUEST_FORBIDDEN on
tenant resolution, step 1 is different: fetch a fresh join link (a new
invite), rejoin from it, and only then re-post.
When to escalate — the support bundle
Work the sections first — most refusals are a stale invite or an un-admitted guest. When a503 persists past a few minutes, or a guest
refusal survives a fresh join link and a confirmed host admit, open a
ticket with:
request_id— from themeta.request_idon the refused response.- Room id — the UUID in the route path.
- Error code verbatim —
VIDEO_CHAT_GUEST_FORBIDDEN,VIDEO_CHAT_LOBBY_HELD,VIDEO_CHAT_LOBBY_CHECK_UNAVAILABLE, orVIDEO_CHAT_POLICY_UNAVAILABLE. - Which resolution path the guest was on — an anonymous invite-token send (no signed-in account), or a signed-in sender. The gate behavior differs, and support triages them separately.
- Timestamp in UTC of the refusal, and whether 503s repeated with backoff.
- Lobby state — whether the room had the waiting room armed, and
whether
auto_promotewas set (withauto_promote: truethe first host entry lifts everyone; if no host had joined yet, aLOBBY_HELDrefusal is the lobby working as configured).
See also
- Troubleshooting: video room lifecycle failures
— the host-side lobby toggle (
VIDEO_WAITING_ROOM_FAILED) and admit (VIDEO_WAITING_ROOM_ADMIT_FAILED) codes, join-token mint refusals, and capacity gates. - Video channel: rooms, embeds, recording, and broadcast
— the waiting-room lobby: arming,
lobby_downgradedjoin responses, admits, andauto_promote. - Video room features: chat, polls, Q&A, invites, RTMP
— the chat content-policy block family (
VIDEO_CHAT_BLOCKED_BY_POLICY) and the other feature-level codes. - Error codes reference — the full
VIDEO_*code families.