Skip to content
sanalyer API

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 one route on this page is user-only and acts on the caller’s own voice connection. A bot or OAuth2 bearer credential is refused with 403 ACCESS_DENIED.

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.

ValueDescription
gece-loungeA night lounge with one game table, T01, and four watch seats at it
kahvehaneA coffee house with three game tables, T01 to T03, and two watch seats at each
oyun-odasiA 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.

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

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.

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 rack null.
  • 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.

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.

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.

FieldTypeDescription
seat?stringThe seat the connection holds, or null when it stands
PUT/v1/channels/{channel_id}/voice-seat

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

  • 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_id passes the voice membership check.
  • A player seated at an active table cannot move to a lounge or watch seat until the game ends.
FieldTypeDescription
channel_idsnowflakeThe ID of the voice channel
FieldTypeDescription
connection_idstringThe caller’s voice connection in this channel (1-64 characters)
seat?stringA lounge or watch seat of the room, or null to let the server pick a free lounge seat
FieldTypeDescription
seat?stringThe 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.

StatusBodyCondition
200voice seat objectThe connection holds the returned seat, or stands
400error responseThe channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE
400error responseThe channel has no theme, or the seat is not a lounge or watch seat of it, as VOICE_SEAT_UNKNOWN
400error responseThe connection is not the caller’s connection in this channel, returned as USER_NOT_IN_VOICE
401error responseNo account is signed in
403error responseThe credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED
403error response3D rooms are switched off, returned as FEATURE_TEMPORARILY_DISABLED
403error responseThe caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS
404error responseThe channel does not exist, returned as UNKNOWN_CHANNEL
409error responseAnother connection sits on the seat, returned as VOICE_SEAT_TAKEN
409error responseThe caller sits at an active table, returned as TABLE_GAME_CONFLICT
429error responseThe channel’s cap of 20 seat changes a second is spent, returned as RATE_LIMITED

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.

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.