17 KiB
REST API Reference
1. Conventions
Base path: /api/v1
Authentication:
- Use JWT access token in header:
Authorization: Bearer <token> - Login/refresh endpoints return
{ access_token, refresh_token, token_type }
Response codes:
200success201created204success, no body400bad request401unauthorized403forbidden404not found409conflict422validation/business rule error429rate limit503external service unavailable (email/storage/readiness)
Common error body:
{
"detail": "Error text"
}
For /health/ready failure:
{
"detail": {
"status": "not_ready",
"db": false,
"redis": true
}
}
2. Enums
ChatType
privategroupchannel
ChatMemberRole
owneradminmember
MessageType
textimagevideoaudiovoicefilecircle_video
Message status events
message_deliveredmessage_read
3. Models (request/response)
3.1 Auth
RegisterRequest
{
"email": "user@example.com",
"name": "Benya",
"username": "benya",
"password": "strongpassword"
}
LoginRequest
{
"email": "user@example.com",
"password": "strongpassword",
"otp_code": "123456"
}
otp_code is optional and used only when 2FA is enabled.
TokenResponse
{
"access_token": "jwt",
"refresh_token": "jwt",
"token_type": "bearer"
}
SessionRead
{
"jti": "uuid",
"created_at": "2026-03-08T10:00:00Z",
"ip_address": "127.0.0.1",
"user_agent": "Mozilla/5.0 ...",
"current": false,
"token_type": "refresh"
}
AuthUserResponse
{
"id": 1,
"email": "user@example.com",
"name": "Benya",
"username": "benya",
"bio": "optional",
"avatar_url": "https://...",
"email_verified": true,
"twofa_enabled": false,
"allow_private_messages": true,
"privacy_private_messages": "everyone",
"privacy_last_seen": "everyone",
"privacy_avatar": "everyone",
"privacy_group_invites": "everyone",
"created_at": "2026-03-08T10:00:00Z",
"updated_at": "2026-03-08T10:00:00Z"
}
3.2 Users
UserRead
{
"id": 1,
"name": "Benya",
"username": "benya",
"email": "user@example.com",
"avatar_url": "https://...",
"bio": "optional",
"email_verified": true,
"allow_private_messages": true,
"privacy_private_messages": "everyone",
"privacy_last_seen": "everyone",
"privacy_avatar": "everyone",
"privacy_group_invites": "everyone",
"twofa_enabled": false,
"created_at": "2026-03-08T10:00:00Z",
"updated_at": "2026-03-08T10:00:00Z"
}
UserSearchRead
{
"id": 2,
"name": "Other User",
"username": "other",
"email": "other@example.com",
"avatar_url": null
}
UserProfileUpdate
{
"name": "New Name",
"username": "new_username",
"bio": "new bio",
"avatar_url": "https://...",
"allow_private_messages": true,
"privacy_private_messages": "contacts",
"privacy_last_seen": "contacts",
"privacy_avatar": "everyone",
"privacy_group_invites": "contacts"
}
All fields are optional.
privacy_private_messages: everyone | contacts | nobody.
3.3 Chats
ChatRead
{
"id": 10,
"public_id": "A1B2C3D4E5F6G7H8J9K0L1M2",
"type": "private",
"title": null,
"avatar_url": null,
"display_title": "Other User",
"handle": null,
"description": null,
"is_public": false,
"is_saved": false,
"archived": false,
"pinned": false,
"unread_count": 3,
"unread_mentions_count": 1,
"pinned_message_id": null,
"members_count": 2,
"online_count": 1,
"subscribers_count": null,
"counterpart_user_id": 2,
"counterpart_name": "Other User",
"counterpart_username": "other",
"counterpart_avatar_url": "https://...",
"counterpart_is_online": true,
"counterpart_last_seen_at": "2026-03-08T10:00:00Z",
"last_message_text": "Hello",
"last_message_type": "text",
"last_message_created_at": "2026-03-08T10:01:00Z",
"my_role": "member",
"created_at": "2026-03-08T09:00:00Z"
}
ChatCreateRequest
{
"type": "group",
"title": "My Group",
"handle": "mygroup",
"description": "optional",
"is_public": true,
"member_ids": [2, 3]
}
Rules:
private: requires exactly 1 target inmember_idsgroup/channel: requiretitleprivatecannot be public- public chat requires
handle
ChatDetailRead
ChatRead + members[], where member item is:
{
"id": 1,
"user_id": 2,
"role": "member",
"joined_at": "2026-03-08T09:00:00Z"
}
3.4 Messages
MessageRead
{
"id": 100,
"chat_id": 10,
"sender_id": 1,
"reply_to_message_id": null,
"forwarded_from_message_id": null,
"type": "text",
"text": "Hello",
"delivery_status": "read",
"attachment_waveform": [4, 7, 10, 9, 6],
"created_at": "2026-03-08T10:02:00Z",
"updated_at": "2026-03-08T10:02:00Z"
}
MessageCreateRequest
{
"chat_id": 10,
"type": "text",
"text": "Hello",
"client_message_id": "client-msg-0001",
"reply_to_message_id": null
}
MessageForwardRequest
{
"target_chat_id": 20,
"include_author": true
}
MessageForwardBulkRequest
{
"target_chat_ids": [20, 21],
"include_author": true
}
MessageStatusUpdateRequest
{
"chat_id": 10,
"message_id": 100,
"status": "message_read"
}
MessageReactionToggleRequest
{
"emoji": "👍"
}
3.5 Media
UploadUrlRequest
{
"file_name": "photo.jpg",
"file_type": "image/jpeg",
"file_size": 123456
}
UploadUrlResponse
{
"upload_url": "https://...signed...",
"file_url": "https://.../bucket/uploads/....jpg",
"object_key": "uploads/....jpg",
"expires_in": 900,
"required_headers": {
"Content-Type": "image/jpeg"
}
}
AttachmentCreateRequest
{
"message_id": 100,
"file_url": "https://.../bucket/uploads/....jpg",
"file_type": "image/jpeg",
"file_size": 123456,
"waveform_points": [4, 7, 10, 9, 6]
}
AttachmentRead
{
"id": 1,
"message_id": 100,
"file_url": "https://...",
"file_type": "image/jpeg",
"file_size": 123456,
"waveform_points": [4, 7, 10, 9, 6]
}
ChatAttachmentRead
{
"id": 1,
"message_id": 100,
"sender_id": 1,
"message_type": "image",
"message_created_at": "2026-03-08T10:10:00Z",
"file_url": "https://...",
"file_type": "image/jpeg",
"file_size": 123456
}
3.6 Search and notifications
GlobalSearchRead
{
"users": [],
"chats": [],
"messages": []
}
NotificationRead
{
"id": 1,
"user_id": 1,
"event_type": "message_created",
"payload": "{\"chat_id\":10}",
"created_at": "2026-03-08T10:15:00Z"
}
4. Health endpoints
GET /health
Returns:
{ "status": "ok" }
GET /health/live
Returns:
{ "status": "ok" }
GET /health/ready
Returns:
{ "status": "ready", "db": "ok", "redis": "ok" }
5. Auth endpoints
POST /api/v1/auth/register
Body: RegisterRequest
Response: 201 + MessageResponse
POST /api/v1/auth/login
Body: LoginRequest
Response: 200 + TokenResponse
POST /api/v1/auth/refresh
Body:
{ "refresh_token": "jwt" }
Response: 200 + TokenResponse
POST /api/v1/auth/verify-email
Body:
{ "token": "verification_token" }
Response: 200 + MessageResponse
POST /api/v1/auth/resend-verification
Body:
{ "email": "user@example.com" }
Response: 200 + MessageResponse
POST /api/v1/auth/request-password-reset
Body:
{ "email": "user@example.com" }
Response: 200 + MessageResponse
POST /api/v1/auth/reset-password
Body:
{
"token": "reset_token",
"new_password": "newStrongPassword"
}
Response: 200 + MessageResponse
GET /api/v1/auth/me
Auth required.
Response: 200 + AuthUserResponse
GET /api/v1/auth/sessions
Auth required.
Response: 200 + SessionRead[]
Note: list includes refresh sessions and a synthetic current access-token session (token_type=access) for stable UI visibility.
DELETE /api/v1/auth/sessions/{jti}
Auth required.
Response: 204
DELETE /api/v1/auth/sessions
Auth required.
Response: 204
Behavior: revokes all refresh sessions, invalidates all access tokens issued before this request, and force-closes active realtime WebSocket connections for the user.
POST /api/v1/auth/2fa/setup
Auth required.
Response:
{
"secret": "BASE32SECRET",
"otpauth_url": "otpauth://..."
}
If 2FA is already enabled for the account, returns 400 ("2FA is already enabled").
POST /api/v1/auth/2fa/enable
Auth required.
Body:
{ "code": "123456" }
Response: 200 + MessageResponse
POST /api/v1/auth/2fa/disable
Auth required.
Body:
{ "code": "123456" }
Response: 200 + MessageResponse
6. Users endpoints
GET /api/v1/users/me
Auth required.
Response: 200 + UserRead
GET /api/v1/users/search?query=<text>&limit=20
Auth required.
Response: 200 + UserSearchRead[]
Note: if query shorter than 2 chars (after trimming optional leading @) returns empty list.
PUT /api/v1/users/profile
Auth required.
Body: UserProfileUpdate
Response: 200 + UserRead
GET /api/v1/users/blocked
Auth required.
Response: 200 + UserSearchRead[]
GET /api/v1/users/contacts
Auth required.
Response: 200 + UserSearchRead[]
POST /api/v1/users/{user_id}/contacts
Auth required.
Response: 204
POST /api/v1/users/contacts/by-email
Auth required.
Body:
{ "email": "target@example.com" }
Response: 204
DELETE /api/v1/users/{user_id}/contacts
Auth required.
Response: 204
POST /api/v1/users/{user_id}/block
Auth required.
Response: 204
DELETE /api/v1/users/{user_id}/block
Auth required.
Response: 204
GET /api/v1/users/{user_id}
Auth required.
Response: 200 + UserRead
7. Chats endpoints
GET /api/v1/chats
Query params:
limit(default50)before_id(optional)query(optional search by title/type)archived(defaultfalse)
Auth required.
Response: 200 + ChatRead[]
GET /api/v1/chats/saved
Returns or creates personal saved-messages chat.
Auth required.
Response: 200 + ChatRead
GET /api/v1/chats/discover?query=<text>&limit=30
Auth required.
Response: 200 + ChatDiscoverRead[]
POST /api/v1/chats
Auth required.
Body: ChatCreateRequest
Response: 200 + ChatRead
POST /api/v1/chats/{chat_id}/join
Join public group/channel.
Auth required.
Response: 200 + ChatRead
GET /api/v1/chats/{chat_id}
Auth required.
Response: 200 + ChatDetailRead
PATCH /api/v1/chats/{chat_id}/title
Auth required.
Body:
{ "title": "New title" }
Response: 200 + ChatRead
PATCH /api/v1/chats/{chat_id}/profile
Auth required.
Body (all fields optional):
{
"title": "New title",
"description": "optional description",
"avatar_url": "https://..."
}
Rules:
- only for
group/channel - only for
owner/admin
Response: 200 + ChatRead
GET /api/v1/chats/{chat_id}/members
Auth required.
Response: 200 + ChatMemberRead[]
POST /api/v1/chats/{chat_id}/members
Auth required.
Body:
{ "user_id": 123 }
Response: 201 + ChatMemberRead
PATCH /api/v1/chats/{chat_id}/members/{user_id}/role
Auth required.
Body:
{ "role": "admin" }
Response: 200 + ChatMemberRead
DELETE /api/v1/chats/{chat_id}/members/{user_id}
Auth required.
Response: 204
POST /api/v1/chats/{chat_id}/bans/{user_id}
Auth required (owner/admin in group/channel).
Response: 204
Behavior: bans user from chat and removes membership if present.
DELETE /api/v1/chats/{chat_id}/bans/{user_id}
Auth required (owner/admin in group/channel).
Response: 204
POST /api/v1/chats/{chat_id}/leave
Auth required.
Response: 204
DELETE /api/v1/chats/{chat_id}?for_all=false
Auth required.
Response: 204
POST /api/v1/chats/{chat_id}/clear
Clear chat history for current user.
Auth required.
Response: 204
POST /api/v1/chats/{chat_id}/pin
Pin chat message.
Body:
{ "message_id": 100 }
or unpin:
{ "message_id": null }
Response: 200 + ChatRead
GET /api/v1/chats/{chat_id}/notifications
Auth required.
Response:
{
"chat_id": 10,
"user_id": 1,
"muted": false
}
PUT /api/v1/chats/{chat_id}/notifications
Auth required.
Body:
{ "muted": true }
Response: 200 + ChatNotificationSettingsRead
Note: mentions (@username) are delivered even when chat is muted.
3.7 Privacy enforcement notes
- User profile privacy is enforced server-side:
privacy_avatarcontrols whether other users receiveavatar_url.privacy_last_seencontrols whether private-chat counterpart presence fields are visible:counterpart_is_onlinecounterpart_last_seen_at
- For
contactsmode, visibility is granted only when the viewer is in target user's contacts. - Group/channel invite restrictions are enforced by
privacy_group_invites:- Users with
contactscan be added only by users present in their contacts list. - Applies to group/channel creation with initial members and admin add-member action.
- Users with
POST /api/v1/chats/{chat_id}/archive
Auth required.
Response: 200 + ChatRead
POST /api/v1/chats/{chat_id}/unarchive
Auth required.
Response: 200 + ChatRead
POST /api/v1/chats/{chat_id}/pin-chat
Auth required.
Response: 200 + ChatRead
POST /api/v1/chats/{chat_id}/unpin-chat
Auth required.
Response: 200 + ChatRead
POST /api/v1/chats/{chat_id}/invite-link
Auth required.
Response:
{
"chat_id": 10,
"token": "invite_token",
"invite_url": "https://frontend/join?token=invite_token"
}
POST /api/v1/chats/join-by-invite
Auth required.
Body:
{ "token": "invite_token" }
Response: 200 + ChatRead
8. Messages endpoints
POST /api/v1/messages
Auth required.
Body: MessageCreateRequest
Response: 201 + MessageRead
GET /api/v1/messages/search?query=<text>&chat_id=<id>&limit=50
Auth required.
Response: 200 + MessageRead[]
GET /api/v1/messages/{chat_id}?limit=50&before_id=<message_id>
Auth required.
Response: 200 + MessageRead[]
GET /api/v1/messages/{message_id}/thread?limit=100
Auth required.
Returns root message + nested replies (thread subtree, BFS by reply links).
Response: 200 + MessageRead[]
PUT /api/v1/messages/{message_id}
Auth required.
Body:
{ "text": "Edited text" }
Response: 200 + MessageRead
DELETE /api/v1/messages/{message_id}?for_all=false
Auth required.
Response: 204
POST /api/v1/messages/status
Auth required.
Body: MessageStatusUpdateRequest
Response:
{
"last_delivered_message_id": 123,
"last_read_message_id": 120
}
POST /api/v1/messages/{message_id}/forward
Auth required.
Body: MessageForwardRequest
Response: 201 + MessageRead
POST /api/v1/messages/{message_id}/forward-bulk
Auth required.
Body: MessageForwardBulkRequest
Response: 201 + MessageRead[]
GET /api/v1/messages/{message_id}/reactions
Auth required.
Response:
[
{ "emoji": "👍", "count": 3, "reacted": true }
]
POST /api/v1/messages/{message_id}/reactions/toggle
Auth required.
Body: MessageReactionToggleRequest
Response: 200 + MessageReactionRead[]
9. Media endpoints
POST /api/v1/media/upload-url
Auth required.
Body: UploadUrlRequest
Response: 200 + UploadUrlResponse
Validation:
- Allowed MIME:
image/jpeg,image/png,image/webp,video/mp4,video/webm,audio/mpeg,audio/ogg,audio/webm,audio/wav,application/pdf,application/zip,text/plain - Max size:
MAX_UPLOAD_SIZE_BYTES
POST /api/v1/media/attachments
Auth required.
Body: AttachmentCreateRequest
Response: 200 + AttachmentRead
Rules:
file_urlmust point to configured S3 bucket endpoint- Only message sender can attach files
GET /api/v1/media/chats/{chat_id}/attachments?limit=100&before_id=<id>
Auth required.
Response: 200 + ChatAttachmentRead[]
10. Notifications
GET /api/v1/notifications?limit=50
Auth required.
Response: 200 + NotificationRead[]
11. Global search
GET /api/v1/search?query=<text>&users_limit=10&chats_limit=10&messages_limit=10
Auth required.
Response: 200 + GlobalSearchRead
12. Rate limits
Configured via env vars:
LOGIN_RATE_LIMIT_PER_MINUTEfor/auth/loginREGISTER_RATE_LIMIT_PER_MINUTEfor/auth/registerREFRESH_RATE_LIMIT_PER_MINUTEfor/auth/refreshRESET_RATE_LIMIT_PER_MINUTEfor reset/resend flowsMESSAGE_RATE_LIMIT_PER_MINUTEandDUPLICATE_MESSAGE_COOLDOWN_SECONDSfor sending messages
429 response example:
{
"detail": "Rate limit exceeded. Retry in 34 seconds."
}
13. Notes
public_idis returned for chats and should be used by clients as stable public identifier.- Invite links are generated for group/channel chats.
- In channels, only users with sufficient role (owner/admin) can post.
emailrouter exists in codebase but has no public REST endpoints yet.