Voice seats
A guild voice channel can carry a room theme, which a client can show as a 3D room. The room has named seats, and each voice connection in the channel is either seated or standing. The server owns every seat: it seats a connection when it joins, frees the seat when the connection leaves, and carries each connection’s seat to the rest of the room as a LiveKit participant attribute. The tables in the room are played through Table games.
The routes on this page are user-only and act on the caller’s own voice connection. A bot or OAuth2 bearer credential is refused with 403 ACCESS_DENIED.
Room themes
Section titled “Room themes”Modify channel sets the theme as room_theme, and the channel object reports it. A channel with no theme has no room. This server version defines three themes.
| Value | Description |
|---|---|
| gece-lounge | A night lounge with one game table, T01, and four watch seats at it |
| kahvehane | A coffee house with three game tables, T01 to T03, and two watch seats at each |
| oyun-odasi | A game room with two game tables, T01 and T02, and four watch seats at each |
A theme fixes the lounge seats, the table slots, the watch seats, and the standing places of the room. The client draws them from the theme’s room file, so a theme adds no field to the channel beyond its ID.
Seat names
Section titled “Seat names”| Value | Description |
|---|---|
seat_<G>_<NN> | A lounge seat, where G is a seat group in capital letters and digits and NN two digits, such as seat_A_01 |
seat_T<NN>_<K> | A seat at a game table, where TNN is the table slot and K the position 1-4, such as seat_T01_2 |
seat_T<NN>_Y<K> | A watch seat beside a game table, where K is 1-4, such as seat_T01_Y2 |
A connection on no seat is standing. Table seats are taken by sitting down at a table and are never named to Take a voice seat.
Seat assignment
Section titled “Seat assignment”One account holds at most one seat in a channel, whichever of its connections sits on it.
- When LiveKit confirms that a connection joined, the server seats it on the first free lounge seat in the theme’s order. A player at an active table is seated at the table instead, and a connection that already holds a seat keeps it.
- A second connection of the same account stays standing. Taking a seat from it moves the account’s seat to it and stands the first connection up.
- When a connection leaves, its seat passes to the account’s most recently joined connection that is still in the channel and standing, which keeps the seat across a page reload. Otherwise the seat becomes free.
- A connection that finds the room full stands. The server then frees the seats of connections that are no longer in the channel, at most once per channel every 30 seconds, and tries again.
Sitting down at a game table moves the player’s seat to the table, and the end of the game moves it back to a free lounge seat. The server seats nobody in a channel without a theme.
Watch seats
Section titled “Watch seats”A watch seat sits beside a game table, behind a player’s chair, so that its holder can follow the game from up close. The server never gives one by itself: a connection takes it only by naming it to Take a voice seat, whether or not a game runs at the table.
- A watch seat carries no right at the table. Its holder is a spectator like anyone else, watches through Watch a table game, and receives the public view with
racknull. - A held watch seat stays with its connection when LiveKit reports the same join again.
- Sitting down at the table from a watch seat moves the seat to the table, and the end of the game moves it to a free lounge seat, not back to the watch seat.
- A player at an active table cannot move to a watch seat until the game ends.
- When the channel’s theme changes, a held lounge or watch seat the new theme lacks is dropped the next time the server seats that connection, which then takes a free lounge seat or stands.
Seat attribute
Section titled “Seat attribute”The seat reaches the room through the LiveKit participant attribute micup.seat of each participant. Its value is the seat name, and a participant without the attribute, or with an empty one, is standing. A client follows the attribute on the participants it already receives and needs no extra request to show who sits where.
Character attribute
Section titled “Character attribute”While room characters are on, each participant also carries the LiveKit participant attribute micup.character, which names the 3D character the client draws for it. Its value is <model_key>;<rrggbb>: the model_key of the worn room character, a ready-made outfit, or the default outfit’s preset key, then a semicolon and the six lowercase hexadecimal digits of the shirt colour, such as ithp-gunluk;d9a441. A participant without the attribute, with a value of another shape or with a key the client does not know, is drawn in the default outfit.
The server writes it when LiveKit confirms a join, and again on Refresh my voice room character. The value comes from the account and is never read from a request. A character the account may no longer wear, or a model this server version does not know, becomes the default outfit.
In the same write the server sets micup.outfit, the parts the account wears with Dress my room character. Its value is 1; and the part keys in slot order (face, hair, top, bottom, shoes, costume, hat, glasses, accessory), joined by commas, such as 1;ith-face-male-usual-001,ith-hair-male-010: at most 9 keys of 1 to 32 lowercase letters, digits and dashes. An empty value removes the attribute: the account wears no part, or wears a room character (a ready-made outfit), which is then drawn instead. A client draws the parts over the default outfit and drops a key it does not know; one that does not read micup.outfit, or reads a value of another shape or version, draws micup.character.
Video attribute
Section titled “Video attribute”A participant who plays a video on the room TV carries the LiveKit participant attribute micup.watch, which every client of the room reads to show that video on its own TV. Its value is yt:<video id>|<started at>|<paused at>|<written at>: the 11-character YouTube video ID, then three times in milliseconds on the server clock. started at is when the video would have begun to play from its start, paused at is when it was paused and empty while it plays, and written at is when the server wrote the value, such as yt:dQw4w9WgXcQ|1760000000000||1760000090000. The shared position is the server time now less started at, or paused at less started at while paused. A participant without the attribute, or with a value of another shape, plays nothing.
The server writes it on Play a video on the room TV and removes it on the two stop routes. The value goes with the connection when it leaves, and the server removes it when the participant loses STREAM. A client shows no video of a server-muted participant.
A value whose written at is before watch_since of the voice_room assignment of Get experiment assignments is stale: room TV videos were switched off and on again since it was written, and a client shows nothing for it. A client that sees the value change while it watches can take written at less the time it received the change as its offset to the server clock.
Voice membership
Section titled “Voice membership”The routes here and on Table games take a connection_id, the voice state connection_id of one of the caller’s own connections. The request counts only when that connection is in the channel the route names. The server checks it against the join LiveKit confirmed, with no Gateway call.
A request sent in the moment before LiveKit confirms the join falls back to the Gateway voice states, at most once per channel every 30 seconds. A request that loses that window returns 400 USER_NOT_IN_VOICE, and the same request succeeds once the join is confirmed.
Voice seat object
Section titled “Voice seat object”| Field | Type | Description |
|---|---|---|
| seat | ?string | The seat the connection holds, or null when it stands |
Sealed room files
Section titled “Sealed room files”The room, table part and character part files a client draws come from the static CDN under /rooms/, named <name>.<12 hex>.mglb, where the hex digits are the first 12 of the file’s SHA-256. Each one is sealed: the 4 bytes MGLB, one byte naming the key version, a 12-byte nonce, then the AES-256-GCM ciphertext of the GLB file with its 16-byte tag, the first 5 bytes as additional authenticated data. A client gets the keys from Get the keys of the 3D room files once a session, opens each file in memory and never stores the opened file. A file sealed with a key the client was not given, or with any byte changed, does not open, and the room falls back to the flat view.
Room file key object
Section titled “Room file key object”| Field | Type | Description |
|---|---|---|
| version | integer | The key version (1-255), the fifth byte of a file sealed with this key |
| key | string | The AES-256-GCM key, base64 of 32 bytes |
Get the keys of the 3D room files
Section titled “Get the keys of the 3D room files”GET/v1/voice-rooms/asset-keysReturns the keys that open the sealed room files, the current one first, to any signed-in user account, whether or not 3D rooms reach it: the shop and the outfit editor draw the same files. The server keeps the previous key listed while files sealed with it are still in use.
Response body
Section titled “Response body”| Field | Type | Description |
|---|---|---|
| keys | array of room file key objects | The keys, the current one first |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | response body | The server has a key |
| 401 | error response | No account is signed in |
| 403 | error response | The credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED |
| 403 | error response | The server has no key, returned as FEATURE_TEMPORARILY_DISABLED |
| 429 | error response | The rate limit below is spent, returned as RATE_LIMITED |
Response headers
Section titled “Response headers”| Field | Type | Description |
|---|---|---|
| Cache-Control | string | The literal value private, no-store |
Rate limit
Section titled “Rate limit”10 requests per minute for each authenticated user, on the voice_room:asset_keys bucket.
Take a voice seat
Section titled “Take a voice seat”PUT/v1/channels/{channel_id}/voice-seatSeats the caller’s connection on a lounge or watch seat of the channel’s room and returns the voice seat object. With seat null the server picks a lounge seat, which is how a client asks for a seat when the automatic one did not arrive.
Limitations
Section titled “Limitations”- The caller needs the access Get channel needs, and CONNECT in the channel.
- The channel is a guild voice channel with a room theme.
connection_idpasses the voice membership check.- A player seated at an active table cannot move to a lounge or watch seat until the game ends.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the voice channel |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| connection_id | string | The caller’s voice connection in this channel (1-64 characters) |
| seat | ?string | A lounge or watch seat of the room, or null to let the server pick a free lounge seat |
Response body
Section titled “Response body”| Field | Type | Description |
|---|---|---|
| seat | ?string | The seat the connection now holds, or null when it stands |
When seat is null and the room is full, or the account’s seat is held by another of its connections that is still in the channel, the response is 200 with seat null.
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 200 | voice seat object | The connection holds the returned seat, or stands |
| 400 | error response | The channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE |
| 400 | error response | The channel has no theme, or the seat is not a lounge or watch seat of it, as VOICE_SEAT_UNKNOWN |
| 400 | error response | The connection is not the caller’s connection in this channel, returned as USER_NOT_IN_VOICE |
| 401 | error response | No account is signed in |
| 403 | error response | The credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED |
| 403 | error response | 3D rooms are switched off, returned as FEATURE_TEMPORARILY_DISABLED |
| 403 | error response | The caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS |
| 404 | error response | The channel does not exist, returned as UNKNOWN_CHANNEL |
| 409 | error response | Another connection sits on the seat, returned as VOICE_SEAT_TAKEN |
| 409 | error response | The caller sits at an active table, returned as TABLE_GAME_CONFLICT |
| 429 | error response | The channel’s cap of 20 seat changes a second is spent, returned as RATE_LIMITED |
Side effects
Section titled “Side effects”The server writes the seat attribute of the caller’s connection. A connection that loses its seat to this request, the caller’s own other connection, has its attribute removed. When two requests move the same connection close together, the attribute ends on the seat the server stored last.
Rate limit
Section titled “Rate limit”10 requests per 10 seconds for each authenticated user and channel ID, on the voice_seat:take::channel_id bucket.
The channel as a whole also takes at most 20 seat changes in each one-second window, from all of its members together, because every change reaches each participant of the room. Every request that passes the voice membership check counts, including one that changes nothing, and a request that fails the check never does. The seats the server gives on a LiveKit join are not counted. A request past the cap returns 429 RATE_LIMITED with a retry_after of 1 second and X-RateLimit-Scope: shared, and without X-RateLimit-Bucket.
Refresh my voice room character
Section titled “Refresh my voice room character”POST/v1/channels/{channel_id}/voice-seat/characterShows the character the account wears now on the caller’s connection, which a client sends after Choose a room character. The server writes the character attribute from the account’s cosmetics and takes nothing from the request.
Limitations
Section titled “Limitations”- The caller needs the access Get channel needs, and CONNECT in the channel.
- The channel is a guild voice channel with a room theme.
connection_idpasses the voice membership check.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the voice channel |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| connection_id | string | The caller’s voice connection in this channel (1-64 characters) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | none | The attribute was written |
| 400 | error response | The channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE |
| 400 | error response | The channel has no theme, returned as VOICE_SEAT_UNKNOWN |
| 400 | error response | The connection is not the caller’s connection in this channel, returned as USER_NOT_IN_VOICE |
| 401 | error response | No account is signed in |
| 403 | error response | The credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED |
| 403 | error response | Room characters are switched off, returned as FEATURE_TEMPORARILY_DISABLED |
| 403 | error response | The caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS |
| 404 | error response | The channel does not exist, returned as UNKNOWN_CHANNEL |
Rate limit
Section titled “Rate limit”6 requests per minute for each authenticated user and channel ID, on the voice_room:character::channel_id bucket.
Play a video on the room TV
Section titled “Play a video on the room TV”PUT/v1/channels/{channel_id}/voice-seat/watchPlays the YouTube video of url on the room TV for everyone in the voice channel, from position_ms, playing or paused, and returns 204 with an empty body. The same call seeks, pauses and resumes. The server writes the video attribute of the caller’s connection, so the video ends when that connection leaves.
Limitations
Section titled “Limitations”- Room TV videos are switched on for the instance, which needs 3D rooms on.
- The caller needs the access Get channel needs, and CONNECT and STREAM in the channel, and is not server-muted.
- The channel is a guild voice channel with a room theme.
connection_idpasses the voice membership check.urlis anhttpslink to one YouTube video onyoutube.com,www.youtube.com,m.youtube.comormusic.youtube.com(a/watch?v=,/shorts/,/embed/or/live/path), or onyoutu.be, with no user information or port.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the voice channel |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| connection_id | string | The caller’s voice connection in this channel (1-64 characters) |
| url | string | The YouTube video link to play (1-2048 characters) |
| position_ms? | integer | Playback position in milliseconds, 0 to 86400000 (default 0) |
| paused? | boolean | Whether the video stands paused at position_ms (default false) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | none | The video plays from the given position, or stands paused there |
| 400 | error response | The channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE |
| 400 | error response | The channel has no theme, returned as VOICE_SEAT_UNKNOWN |
| 400 | error response | url is not an https YouTube video link, or position_ms is out of range, returned as INVALID_FORM_BODY |
| 400 | error response | The connection is not the caller’s connection in this channel, returned as USER_NOT_IN_VOICE |
| 401 | error response | No account is signed in |
| 403 | error response | The credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED |
| 403 | error response | 3D rooms or room TV videos are switched off, returned as FEATURE_TEMPORARILY_DISABLED |
| 403 | error response | The caller lacks VIEW_CHANNEL, CONNECT or STREAM, or is server-muted, returned as MISSING_PERMISSIONS |
| 404 | error response | The channel does not exist, returned as UNKNOWN_CHANNEL |
| 429 | error response | The rate limit below is spent, returned as RATE_LIMITED |
| 503 | error response | The voice server did not take the write, returned as SERVICE_UNAVAILABLE |
Side effects
Section titled “Side effects”The server writes the video attribute of the caller’s connection and no other attribute. It reads STREAM again after the write: when the caller lost it meanwhile, the server removes the attribute again and the request returns 403 MISSING_PERMISSIONS.
Rate limit
Section titled “Rate limit”20 requests per minute for each authenticated user and channel ID, on the voice_room:watch::channel_id bucket, which the two stop routes below share.
Stop my video on the room TV
Section titled “Stop my video on the room TV”DELETE/v1/channels/{channel_id}/voice-seat/watchStops the video the caller plays from one voice connection, and returns 204 with an empty body. It works while room TV videos are switched off too, so a video started before the switch went off can still be stopped.
Limitations
Section titled “Limitations”- The caller needs the access Get channel needs, and CONNECT in the channel.
- The channel is a guild voice channel.
connection_idpasses the voice membership check.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the voice channel |
JSON body
Section titled “JSON body”| Field | Type | Description |
|---|---|---|
| connection_id | string | The caller’s voice connection that plays the video (1-64 characters) |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | none | The connection plays no video |
| 400 | error response | The channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE |
| 400 | error response | The connection is not the caller’s connection in this channel, returned as USER_NOT_IN_VOICE |
| 401 | error response | No account is signed in |
| 403 | error response | The credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED |
| 403 | error response | The caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS |
| 404 | error response | The channel does not exist, returned as UNKNOWN_CHANNEL |
| 429 | error response | The rate limit below is spent, returned as RATE_LIMITED |
| 503 | error response | The voice server did not take the write, returned as SERVICE_UNAVAILABLE |
Rate limit
Section titled “Rate limit”20 requests per minute for each authenticated user and channel ID, on the voice_room:watch::channel_id bucket shared with Play a video on the room TV.
Stop a member’s video on the room TV
Section titled “Stop a member’s video on the room TV”DELETE/v1/channels/{channel_id}/voice-seat/watch/{user_id}Stops the video another member plays on the room TV for everyone, on every voice connection of theirs in this channel, and returns 204 with an empty body.
Limitations
Section titled “Limitations”- Room TV videos are switched on for the instance, which needs 3D rooms on.
- The caller needs the access Get channel needs, CONNECT, and MUTE_MEMBERS or MOVE_MEMBERS in the channel.
- As for muting or moving the member, the caller needs role hierarchy authority over them. The guild owner holds it over everyone, and no other caller holds it over the owner.
- The channel is a guild voice channel with a room theme, and the member is in it.
Path parameters
Section titled “Path parameters”| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the voice channel |
| user_id | snowflake | The ID of the member whose video stops |
Response
Section titled “Response”| Status | Body | Condition |
|---|---|---|
| 204 | none | The member’s connections in the channel play no video |
| 400 | error response | The channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE |
| 400 | error response | The channel has no theme, returned as VOICE_SEAT_UNKNOWN |
| 400 | error response | The member is not in this voice channel, returned as USER_NOT_IN_VOICE |
| 401 | error response | No account is signed in |
| 403 | error response | The credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED |
| 403 | error response | 3D rooms or room TV videos are switched off, returned as FEATURE_TEMPORARILY_DISABLED |
| 403 | error response | The caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS |
| 403 | error response | The caller has neither moderation permission, or no authority over the member, as MISSING_PERMISSIONS |
| 404 | error response | The channel does not exist, returned as UNKNOWN_CHANNEL |
| 429 | error response | The rate limit below is spent, returned as RATE_LIMITED |
| 503 | error response | The voice server did not take a write, returned as SERVICE_UNAVAILABLE |
Rate limit
Section titled “Rate limit”20 requests per minute for each authenticated user and channel ID, on the voice_room:watch::channel_id bucket shared with Play a video on the room TV.