Base path: /api/v1
Protected routes require: Authorization: Bearer <access_token>
Create a new account.
Request
{ "username": "alice", "email": "alice@example.com", "password": "secret" }Response 201
{ "id": "<uuid>", "username": "alice", "display_name": "alice", "avatar_url": null, "email": "alice@example.com", "created_at": "...", "updated_at": "..." }Request
{ "email": "alice@example.com", "password": "secret" }Response 200
{ "access_token": "<jwt>", "refresh_token": "<opaque>" }Request
{ "refresh_token": "<opaque>" }Response 200
{ "access_token": "<jwt>" }Request
{ "refresh_token": "<opaque>" }Response 204
Create a room. Creator is automatically added as a member.
Request
{ "name": "general", "description": "General discussion" }Response 201
{ "id": "<uuid>", "name": "general", "description": "...", "created_by": "<uuid>", "created_at": "...", "updated_at": "..." }List all rooms the authenticated user is a member of.
Response 200 — array of room objects.
Get a single room by ID.
Response 200 — room object, 404 if not found.
Join a room.
Response 204
Leave a room.
Response 204
Paginated message history, newest first.
Only room members can access this endpoint. Non-members receive 403.
Query params
| Param | Type | Default | Description |
|---|---|---|---|
| before | RFC3339Nano | now | Return messages older than this time |
| limit | int (1–100) | 50 | Number of messages to return |
Response 200 — array of message objects.
[
{
"id": "<uuid>",
"room_id": "<uuid>",
"sender_id": "<uuid>",
"content": "Hello",
"created_at": "...",
"edited_at": "...",
"deleted_at": "..."
}
]List members of a room.
Only room members can access this endpoint. Non-members receive 403.
Response 200
[
{
"user_id": "<uuid>",
"username": "alice",
"display_name": "Alice",
"avatar_url": null,
"role": "owner",
"joined_at": "...",
"last_seen": null,
"online": true
}
]Presence semantics:
online=trueimplieslast_seen=nullonline=falsemay include the latest persistedlast_seen
Role semantics:
- room creator is
owner - joined users are
member
Remove a room member.
Only the room owner can remove members.
Rules:
- owner cannot be removed
- use
POST /rooms/:id/leavefor self-removal
Response 204
Returns unread room message counts for the authenticated user, grouped by room.
Response 200
[
{ "room_id": "<uuid>", "count": 5 },
{ "room_id": "<uuid>", "count": 0 }
]Count rules:
- Only rooms where the user is a member are included.
- Only messages from other users are counted (
sender_id != current_user). - The unread boundary is the latest of:
- room read state (
last_read_at) if present - otherwise the room membership join time (
joined_at)
- room read state (
Join backlog rule:
- Messages sent before the user joined a room are not counted unread.
Returns room conversation rows for the authenticated user, ordered by latest room activity.
Each row includes:
- room metadata (
room_id,name,description) member_countsnapshot for current room membersonline_countsnapshot for current room membersunread_countlast_message(ornullfor rooms with no messages)
Query params
| Param | Type | Default | Description |
|---|---|---|---|
| limit | int(1-100) | 50 | Max number of room rows |
Response 200
[
{
"room_id": "<uuid>",
"name": "general",
"description": "General discussion",
"member_count": 12,
"online_count": 3,
"unread_count": 3,
"last_message": {
"id": "<uuid>",
"room_id": "<uuid>",
"sender_id": "<uuid>",
"content": "latest text",
"created_at": "...",
"edited_at": "...",
"deleted_at": null
}
}
]Ordering rules:
- Rooms are sorted by latest message timestamp descending.
- Rooms with no messages are listed after active rooms.
Paginated DM history between the authenticated user and :userId, newest first.
Requests where :userId equals the authenticated user are rejected with 400.
Query params — same as room messages (before, limit).
Response 200 — array of direct message objects.
[
{
"id": "<uuid>",
"sender_id": "<uuid>",
"receiver_id": "<uuid>",
"content": "Hey",
"created_at": "...",
"delivered_at": "...",
"read_at": "...",
"edited_at": "...",
"deleted_at": "..."
}
]delivered_atis set when the recipient WebSocket connection receives the DM event.read_atis set when the recipient marks the DM as read over WebSocket.edited_atis set when the sender edits the message.deleted_atis set when the sender soft deletes the message.
Returns unread inbound DM counts grouped by sender user.
Response 200
[
{ "user_id": "<uuid>", "count": 3 },
{ "user_id": "<uuid>", "count": 1 }
]Only messages where the authenticated user is the receiver_id and read_at is null are counted.
Returns the DM conversation list for the authenticated user.
Each item includes:
- conversation partner user
- partner online status snapshot (
online) - partner last seen snapshot (
last_seen) - latest message in that thread
- unread count for inbound DMs from that partner
Query params
| Param | Type | Default | Description |
|---|---|---|---|
| limit | int(1-100) | 50 | Max number of conversations |
Response 200
[
{
"user_id": "<uuid>",
"username": "alice",
"display_name": "Alice",
"avatar_url": null,
"online": true,
"last_seen": null,
"unread_count": 3,
"last_message": {
"id": "<uuid>",
"sender_id": "<uuid>",
"receiver_id": "<uuid>",
"content": "See you soon",
"created_at": "...",
"delivered_at": "...",
"read_at": "...",
"edited_at": "...",
"deleted_at": null
}
}
]Presence semantics:
online=trueimplieslast_seen=nullonline=falsemay include the latest persistedlast_seen
Returns 200 ok. Used by Kubernetes probes.
WebSocket upgrade endpoint. See WebSocket Protocol.