Rooms with the Dart SDK
Room lifecycle and moderation are covered by two SDK surfaces:
client.rooms— the typed REST facade (create, list, join grants, moderation, recordings).client.joinRoom()/RoomHandle— the high-level path that joins a room, wires signaling + media, and gives you live events and in-room moderation methods.
Most apps only need joinRoom() plus a few client.rooms calls.
Create, list, get, delete
final created = await client.rooms.create(CreateRoomRequest(
name: 'Team Standup',
slug: 'team-standup', // optional
maxParticipants: 10, // optional (default 50)
));
final room = created.room; // Room: id, name, slug, status, ...
final page = await client.rooms.list(status: 'active', limit: 20, offset: 0);
// page.data -> List<Room>, page.pagination -> PaginationMeta
final one = await client.rooms.get(room.id); // throws NotFoundException on 404
await client.rooms.delete(room.id); // soft-delete
Join a room
The recommended path is the client-level orchestration:
final handle = await client.joinRoom(roomId,
options: const JoinOptions(audio: true, telemetry: true));
handle.events.listen((event) {
switch (event) {
case RosterSnapshot(:final participants):
// full roster (also available any time as handle.participants)
case ParticipantJoined(:final participant):
// participant.displayName, .role, .muted, .speaking
case ParticipantLeft(:final participant):
// ...
case RoomClosing():
// the room is shutting down
default:
break;
}
});
joinRoom() calls the join endpoint, opens the signaling WebSocket, waits for the hello handshake, connects LiveKit media with the server-provisioned ICE servers, starts QoS telemetry, and requests an initial roster — then returns the RoomHandle.
If you need the raw join grant (for example, to drive media yourself), call the facade directly:
final grant = await client.rooms.join(roomId);
// grant.sessionId, grant.livekitToken, grant.livekitUrl,
// grant.signalUrl, grant.signalTicket, grant.iceServers
Leave a room
await handle.leave();
leave() stops telemetry, disconnects signaling (sending the bye frame) and media, notifies the server (POST /v1/rooms/{id}/leave, best-effort), and closes the event stream. If you joined via the facade instead, call client.rooms.leave(roomId) yourself.
Participants and roster
// Live roster, maintained from signaling events:
final roster = handle.participants; // List<RosterParticipant>
// One-off REST query (outside a joined room):
final participants = await client.rooms.listParticipants(roomId);
Prefer the live roster over polling — RosterSnapshot, ParticipantJoined, and ParticipantLeft events keep it current (see Signaling).
Moderation
From inside a room, RoomHandle has host/moderator methods:
await handle.muteParticipant(userId, reason: 'Background noise');
await handle.kickParticipant(userId, reason: 'Disruptive');
await handle.setParticipantRole(userId, 'speaker');
await handle.requestSpeak(); // listener raises hand
The full moderation set — including unmute, ban/unban — is on the facade:
await client.rooms.muteParticipant(roomId, userId, MuteRequest(reason: 'Background noise'));
await client.rooms.unmuteParticipant(roomId, userId);
await client.rooms.kickParticipant(roomId, userId, KickRequest(reason: 'Disruptive'));
await client.rooms.banParticipant(roomId, userId, BanRequest(reason: 'Repeated violations'));
await client.rooms.unbanParticipant(roomId, userId);
await client.rooms.setParticipantRole(roomId, userId, SetRoleRequest(role: RoomRole.speaker));
await client.rooms.requestSpeak(roomId);
Moderation actions arrive to affected clients as room events: Muted, Unmuted, RoleChanged, SpeakRequested.
Recordings
final rec = await handle.startRecording(); // or client.rooms.startRecording(roomId)
await handle.stopRecording(); // or client.rooms.stopRecording(roomId)
final recs = await client.rooms.listRecordings(roomId); // List<Recording>
Rooms emit RecordingStarted / RecordingStopped events so every participant can reflect recording state in the UI.
Endpoints without a facade method
A few room-service endpoints do not have a dedicated facade method yet; use the authenticated raw client:
// Pending speak requests (hosts):
final reqs = await client.httpClient.get('/rooms/$roomId/speak-requests');
// Moderation audit log:
final log = await client.httpClient.get('/rooms/$roomId/moderation-log');
// Recording consent policy:
await client.httpClient.put('/tenant/recording-consent',
body: {'consent_mode': 'all_consent'}); // all_consent | host_only | no_consent
await client.httpClient.put('/rooms/$roomId/recording-consent',
body: {'consent_mode': 'host_only'}); // per-room override
Under the hood
The facade maps one-to-one onto the room-service REST contract (all endpoints require Authorization: Bearer <JWT>):
| SDK call | Endpoint |
|---|---|
rooms.create(req) | POST /v1/rooms — name (required), slug, max_participants; returns 201. |
rooms.list(...) | GET /v1/rooms?status=&limit=&offset= |
rooms.get(id) / rooms.delete(id) | GET / DELETE /v1/rooms/{id} |
rooms.join(id) | POST /v1/rooms/{id}/join |
rooms.leave(id) | POST /v1/rooms/{id}/leave |
rooms.listParticipants(id) | GET /v1/rooms/{id}/participants |
rooms.muteParticipant(...) etc. | POST /v1/rooms/{id}/participants/{uid}/mute / unmute / kick / ban / unban — mute, kick, ban take {"reason": "..."}. |
rooms.setParticipantRole(...) | POST /v1/rooms/{id}/participants/{uid}/role — {"role": "host" | "speaker" | "listener"}. |
rooms.requestSpeak(id) | POST /v1/rooms/{id}/request-speak |
rooms.startRecording(id) / stopRecording(id) / listRecordings(id) | POST /v1/rooms/{id}/recordings, POST .../recordings/stop, GET .../recordings |
The join response is the credential bundle for everything real-time:
{
"session_id": "…", // UUID for signaling session resume
"livekit_token": "…", // media token — see Media page
"livekit_url": "…", // LiveKit SFU URL
"signal_url": "…", // WebSocket signaling URL
"signal_ticket": "…", // one-time ticket (st_…) — see Signaling page
"ice_servers": [ ] // STUN/TURN configuration (server-provisioned)
}