Rooms API
Endpoints for creating, listing, and managing chat rooms.
Overview
Chat rooms are either public (visible to all users) or private (visible only to members). Users can create, join, leave, and delete rooms.
Endpoint Summary
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/rooms | List rooms (public + private membership) | No |
| POST | /api/rooms | Create room | Yes |
| GET | /api/rooms/{id} | Get room by ID | No |
| DELETE | /api/rooms/{id} | Delete room | Yes |
| POST | /api/rooms/{id}/join | Join room | Yes |
| POST | /api/rooms/{id}/leave | Leave room | Yes |
| GET | /api/rooms/{id}/users | List active users | No |
| POST | /api/rooms/{id}/typing | Broadcast typing indicator | Yes |
Endpoints
List Rooms
Get all public rooms and private rooms visible to the authenticated user (if provided).
GET /api/rooms?username=alice
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string | No | Legacy compatibility query param (visibility still comes from auth token when present) |
{
"rooms": [
{
"id": "general",
"name": "General Chat",
"type": "public",
"createdAt": 1704067200000,
"userCount": 15
},
{
"id": "abc123",
"name": "Private Room",
"type": "private",
"members": ["alice", "bob"],
"createdAt": 1704153600000,
"userCount": 2
}
]
}
Create Room
Create a new chat room. Requires authentication.
Notes:
- Public room creation is admin-only (
ryo). - Private room creation requires at least one member; creator is auto-included.
POST /api/rooms
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json
{
"type": "private",
"members": ["alice", "bob"],
"name": "Project Discussion" // optional
}
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "public" or "private" |
members | string[] | For private | List of usernames |
name | string | Public | Public room name (private names are generated) |
{
"room": {
"id": "abc123def456",
"name": "@alice, @bob",
"type": "private",
"members": ["alice", "bob"],
"createdAt": 1704067200000,
"userCount": 2
}
}
Get Room
Get details for a specific room.
GET /api/rooms/{id}
Response (200):
{
"room": {
"id": "general",
"name": "General Chat",
"type": "public",
"createdAt": 1704067200000,
"userCount": 15
}
}
Delete Room
Delete a room.
- Public rooms: admin-only (
ryo).
- Private rooms: members can "delete" by leaving; room is removed automatically when <= 1 members remain.
DELETE /api/rooms/{id}
Authorization: Bearer {token}
X-Username: alice
Response (200):
{
"success": true
}
Join Room
Join a room to participate in chat.
POST /api/rooms/{id}/join
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json
{
"username": "alice"
}
Response (200):
{
"success": true
}
Leave Room
Leave a room.
POST /api/rooms/{id}/leave
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json
{
"username": "alice"
}
Response (200):
{
"success": true
}
List Active Users
Get list of users currently active in a room.
GET /api/rooms/{id}/users
Response (200):
{
"users": [
{
"username": "alice",
"joinedAt": 1704067200000
},
{
"username": "bob",
"joinedAt": 1704153600000
}
]
}
Typing Indicator
Broadcast a transient typing indicator to room members over the realtime channel. Requires authentication and write access to the room (members for private rooms).
POST /api/rooms/{id}/typing
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json
{
"isTyping": true
}
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
isTyping | boolean | No | Whether the user is typing (defaults to true; only false clears it) |
{
"success": true
}
Nothing is persisted; the indicator is delivered as a realtime broadcast only.
Room Types
Public Rooms
- Visible to all users in room listings
- Anyone can join without invitation
- Default room type for general discussions
Private Rooms
- Only visible to members
- Created with a specific member list
- Ideal for direct messages or group chats
Listen Together Sessions
"Listen Together" lets users share synchronized music/video playback. One member is the host (session owner) and one connection is the DJ (the playback device whose state everyone syncs to). Sessions support both authenticated members and anonymous listeners (joined via an anonymousId). Connections are disambiguated by an optional clientInstanceId so the same user can join from multiple devices. All mutations broadcast over the session's realtime channel.
Endpoint Summary
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/listen/sessions | List active sessions | No |
| POST | /api/listen/sessions | Create a session | Yes |
| GET | /api/listen/sessions/{id} | Get session state | No |
| POST | /api/listen/sessions/{id}/join | Join a session | Yes |
| POST | /api/listen/sessions/{id}/leave | Leave a session | Yes |
| POST | /api/listen/sessions/{id}/sync | Sync playback state (DJ only) | Yes |
| POST | /api/listen/sessions/{id}/assign-dj | Assign the playback device (host only) | Yes |
| POST | /api/listen/sessions/{id}/transfer-host | Transfer session ownership (host only) | Yes |
| POST | /api/listen/sessions/{id}/remote-command | Send a playback intent to the DJ | Yes |
| POST | /api/listen/sessions/{id}/reaction | Send an emoji reaction | Yes |
*join/leave accept either an authenticated user or an anonymous listener (anonymousId in the body, no auth). Providing both is a 400.
Session Object
interface ListenSession {
id: string;
hostUsername: string;
hostClientInstanceId?: string;
djUsername: string;
djClientInstanceId?: string;
createdAt: number;
currentTrackId: string | null;
currentTrackMeta: { title: string; artist?: string; cover?: string; coverColor?: string } | null;
isPlaying: boolean;
positionMs: number;
lastSyncAt: number;
users: Array<{ username: string; joinedAt: number; isOnline: boolean; clientInstanceId?: string }>;
anonymousListeners?: Array<{ anonymousId: string; joinedAt: number }>;
}
List / Create Sessions
GET /api/listen/sessions
Returns lightweight summaries (stale sessions older than 30 minutes are filtered out), sorted by listener count then recency:
{
"sessions": [
{
"id": "abc123",
"hostUsername": "alice",
"djUsername": "alice",
"createdAt": 1704067200000,
"currentTrackMeta": { "title": "Song", "artist": "Artist" },
"isPlaying": true,
"listenerCount": 3
}
]
}
POST /api/listen/sessions
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json
{ "clientInstanceId": "device-1" }
The creator becomes both host and DJ. Response (201): { "session": ListenSession }.
Get Session
GET /api/listen/sessions/{id}
Response (200): { "session": ListenSession }, or 404 if the session does not exist.
Join / Leave
POST /api/listen/sessions/{id}/join
Authorization: Bearer {token}
X-Username: bob
Content-Type: application/json
{ "clientInstanceId": "device-2" }
Anonymous variant (no auth): { "anonymousId": "anon-xyz" }. Response (200): { "session": ListenSession } for authenticated joins. leave returns { "success": true } (with session when the user remains absent but the session lives on). When the host leaves or the last member leaves, the session ends and a session-ended broadcast is sent.
Sync Playback (DJ only)
POST /api/listen/sessions/{id}/sync
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json
{
"clientInstanceId": "device-1",
"state": {
"currentTrackId": "dQw4w9WgXcQ",
"currentTrackMeta": { "title": "Song", "artist": "Artist" },
"isPlaying": true,
"positionMs": 42000,
"djUsername": "alice",
"djClientInstanceId": "device-1"
}
}
Only the current DJ connection may sync. state.isPlaying (boolean) and state.positionMs (number) are required. The state may also hand off the DJ role to another active member. Response (200): { "success": true }.
Assign DJ / Transfer Host (host only)
POST /api/listen/sessions/{id}/assign-dj
{ "nextDjUsername": "bob", "nextDjClientInstanceId": "device-2" }
POST /api/listen/sessions/{id}/transfer-host
{ "nextHostUsername": "bob", "nextHostClientInstanceId": "device-2" }
Both require the caller to be the current host connection and the target to be an active session member. Response (200): { "success": true, "session": ListenSession }.
Remote Command
Non-DJ members submit playback intents; the DJ client applies them and re-syncs.
POST /api/listen/sessions/{id}/remote-command
{ "action": "play" | "pause" | "seek" | "next" | "previous" | "playTrack", "positionMs": 1000, "trackId": "...", "trackMeta": { ... } }
seek requires positionMs; playTrack requires trackId. The DJ connection itself may not send remote commands. Response (200): { "success": true }.
Reaction
POST /api/listen/sessions/{id}/reaction
{ "emoji": "🎉" }
Requires a logged-in member; emoji must be ≤ 8 characters. Response (200): { "success": true }.
IRC Servers
ryOS can bridge chat rooms to public IRC networks. Admins register IRC server configs; any authenticated user can browse the channels a registered server advertises.
Endpoint Summary
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/irc/servers | List registered IRC servers | No |
| POST | /api/irc/servers | Register an IRC server | Admin (ryo) |
| DELETE | /api/irc/servers/{id} | Remove an IRC server | Admin (ryo) |
| GET | /api/irc/servers/{id}/channels | List channels advertised by a server | Yes |
List / Create Servers
GET /api/irc/servers
{
"servers": [
{ "id": "abc", "label": "Pieter", "host": "irc.pieter.com", "port": 6667, "tls": false, "createdAt": 1704067200000 }
]
}
POST /api/irc/servers
Authorization: Bearer {token}
X-Username: ryo
Content-Type: application/json
{ "label": "Libera", "host": "irc.libera.chat", "port": 6697, "tls": true }
Response (201): { "server": IrcServer }.
Delete Server
DELETE /api/irc/servers/{id} (admin only). The built-in default irc.pieter.com server cannot be deleted (400). Response (200): { "success": true }.
List Channels
GET /api/irc/servers/{id}/channels?limit=500&timeoutMs=15000
Runs an IRC LIST against the configured server via the bridge. limit (1–2000, default 500) and timeoutMs (1000–30000, default 15000) are optional.
{
"server": { "id": "abc", "label": "Pieter", "host": "irc.pieter.com", "port": 6667, "tls": false },
"channels": [
{ "channel": "#pieter", "numUsers": 42, "topic": "welcome" }
],
"truncated": false
}
Returns 503 when the IRC bridge is disabled in the current environment.
Error Responses
| Status | Error | Description |
|---|---|---|
| 400 | Invalid request | Missing or invalid parameters |
| 401 | Authentication required | Token missing or invalid |
| 403 | Forbidden | Not authorized to perform action (admin/member checks) |
| 404 | Room not found | Room ID doesn't exist |
| 429 | Rate limit exceeded | Too many requests |
Related
- Auth API - Authentication endpoints
- Messages API - Message endpoints
- Presence API - Presence tracking