> ## 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 guest chat refusals — VIDEO_CHAT_GUEST_FORBIDDEN, VIDEO_CHAT_LOBBY_HELD, and VIDEO_CHAT_LOBBY_CHECK_UNAVAILABLE

> Diagnose why a guest's chat send into a video room was refused: the fail-closed tenant-resolution gate, the lobby admission gate, and the policy-unavailable path — with the recovery sequence for each.

# 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 on
`POST /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](/troubleshooting/video-features-chat-polls-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 calls
`POST /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 route first tries to resolve a tenant from the caller's session context
(a signed-in sender carries one). For an anonymous guest there is no
session, so it falls back to an invite-token lookup — and from that point
forward the send is gated three times, each gate fail-closed.

## 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_token` from the invite link — the route
  cannot scope a send from the room UUID alone.

**Fix:**

1. 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.
2. If you call the route directly, send both `join_token` *and*
   `livekit_token`; the join token is not optional for anonymous sends.
3. 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.

**Fix:**

1. 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).
2. The guest then re-sends the message — after admission the same
   `livekit_token` passes the gate, because the live permission moved, not
   the token.
3. If the guest is no longer connected at all, have them re-join from the
   invite link first.

A stale-token variant of this refusal returns `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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/video/rooms/9b1d2c3e-44aa-4f12-9c0d-roomid/chat" \
  -H "Content-Type: application/json" \
  -d '{
    "message_id": "msg_01JEXAMPLE00000000000000A1",
    "text": "Hi — can everyone hear me?",
    "join_token": "inv_8f3c…",
    "livekit_token": "eyJhbGciOi…"
  }'
```

The room has the waiting room armed and the host has not admitted this
guest yet. The response:

```json theme={null}
{
  "error": {
    "code": "VIDEO_CHAT_LOBBY_HELD",
    "message": "You can chat once the host admits you to the meeting."
  },
  "meta": { "request_id": "req_01JEXAMPLEREFUSAL0000000" }
}
```

Recovery sequence:

1. The host admits the guest
   (`POST /api/v1/video/rooms-scheduled/9b1d2c3e-…/waiting-room/admit/guest-7f2a`
   or the dashboard **Admit** button).
2. The guest re-sends the same message — no new tokens needed; the admit
   changed their live permission.
3. The second `POST` returns `200` and the message broadcasts into the
   room.

If the first refusal had instead been `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 a `503` persists past a few minutes, or a guest
refusal survives a fresh join link and a confirmed host admit, open a
ticket with:

1. **`request_id`** — from the `meta.request_id` on the refused response.
2. **Room id** — the UUID in the route path.
3. **Error code verbatim** — `VIDEO_CHAT_GUEST_FORBIDDEN`,
   `VIDEO_CHAT_LOBBY_HELD`, `VIDEO_CHAT_LOBBY_CHECK_UNAVAILABLE`, or
   `VIDEO_CHAT_POLICY_UNAVAILABLE`.
4. **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.
5. **Timestamp in UTC** of the refusal, and whether 503s repeated with
   backoff.
6. **Lobby state** — whether the room had the waiting room armed, and
   whether `auto_promote` was set (with `auto_promote: true` the first
   host entry lifts everyone; if no host had joined yet, a `LOBBY_HELD`
   refusal is the lobby working as configured).

## See also

* [Troubleshooting: video room lifecycle failures](/troubleshooting/video-room-lifecycle)
  — 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](/channels/video#waiting-room-the-lobby)
  — the waiting-room lobby: arming, `lobby_downgraded` join responses,
  admits, and `auto_promote`.
* [Video room features: chat, polls, Q\&A, invites, RTMP](/troubleshooting/video-features-chat-polls-rtmp)
  — the chat content-policy block family (`VIDEO_CHAT_BLOCKED_BY_POLICY`)
  and the other feature-level codes.
* [Error codes reference](/reference/error-codes) — the full `VIDEO_*`
  code families.


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