Skip to content
Micup 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 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.

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.

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.

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.

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

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.

FieldTypeDescription
versionintegerThe key version (1-255), the fifth byte of a file sealed with this key
keystringThe AES-256-GCM key, base64 of 32 bytes
GET/v1/voice-rooms/asset-keys

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

FieldTypeDescription
keysarray of room file key objectsThe keys, the current one first
StatusBodyCondition
200response bodyThe server has a key
401error responseNo account is signed in
403error responseThe credential is a bot or OAuth2 bearer token, returned as ACCESS_DENIED
403error responseThe server has no key, returned as FEATURE_TEMPORARILY_DISABLED
429error responseThe rate limit below is spent, returned as RATE_LIMITED
FieldTypeDescription
Cache-ControlstringThe literal value private, no-store

10 requests per minute for each authenticated user, on the voice_room:asset_keys bucket.

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.

POST/v1/channels/{channel_id}/voice-seat/character

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

FieldTypeDescription
channel_idsnowflakeThe ID of the voice channel
FieldTypeDescription
connection_idstringThe caller’s voice connection in this channel (1-64 characters)
StatusBodyCondition
204noneThe attribute was written
400error responseThe channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE
400error responseThe channel has no theme, returned 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 responseRoom characters 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

6 requests per minute for each authenticated user and channel ID, on the voice_room:character::channel_id bucket.

PUT/v1/channels/{channel_id}/voice-seat/watch

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

  • 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_id passes the voice membership check.
  • url is an https link to one YouTube video on youtube.com, www.youtube.com, m.youtube.com or music.youtube.com (a /watch?v=, /shorts/, /embed/ or /live/ path), or on youtu.be, with no user information or port.
FieldTypeDescription
channel_idsnowflakeThe ID of the voice channel
FieldTypeDescription
connection_idstringThe caller’s voice connection in this channel (1-64 characters)
urlstringThe YouTube video link to play (1-2048 characters)
position_ms?integerPlayback position in milliseconds, 0 to 86400000 (default 0)
paused?booleanWhether the video stands paused at position_ms (default false)
StatusBodyCondition
204noneThe video plays from the given position, or stands paused there
400error responseThe channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE
400error responseThe channel has no theme, returned as VOICE_SEAT_UNKNOWN
400error responseurl is not an https YouTube video link, or position_ms is out of range, returned as INVALID_FORM_BODY
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 or room TV videos are switched off, returned as FEATURE_TEMPORARILY_DISABLED
403error responseThe caller lacks VIEW_CHANNEL, CONNECT or STREAM, or is server-muted, returned as MISSING_PERMISSIONS
404error responseThe channel does not exist, returned as UNKNOWN_CHANNEL
429error responseThe rate limit below is spent, returned as RATE_LIMITED
503error responseThe voice server did not take the write, returned as SERVICE_UNAVAILABLE

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.

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.

DELETE/v1/channels/{channel_id}/voice-seat/watch

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

FieldTypeDescription
channel_idsnowflakeThe ID of the voice channel
FieldTypeDescription
connection_idstringThe caller’s voice connection that plays the video (1-64 characters)
StatusBodyCondition
204noneThe connection plays no video
400error responseThe channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE
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 responseThe caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS
404error responseThe channel does not exist, returned as UNKNOWN_CHANNEL
429error responseThe rate limit below is spent, returned as RATE_LIMITED
503error responseThe voice server did not take the write, returned as SERVICE_UNAVAILABLE

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.

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.

  • 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.
FieldTypeDescription
channel_idsnowflakeThe ID of the voice channel
user_idsnowflakeThe ID of the member whose video stops
StatusBodyCondition
204noneThe member’s connections in the channel play no video
400error responseThe channel is not a guild voice channel, returned as INVALID_CHANNEL_TYPE
400error responseThe channel has no theme, returned as VOICE_SEAT_UNKNOWN
400error responseThe member is not in this voice 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 or room TV videos are switched off, returned as FEATURE_TEMPORARILY_DISABLED
403error responseThe caller lacks VIEW_CHANNEL or CONNECT, returned as MISSING_PERMISSIONS
403error responseThe caller has neither moderation permission, or no authority over the member, as MISSING_PERMISSIONS
404error responseThe channel does not exist, returned as UNKNOWN_CHANNEL
429error responseThe rate limit below is spent, returned as RATE_LIMITED
503error responseThe voice server did not take a write, returned as SERVICE_UNAVAILABLE

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.