# REST API Reference ## 1. Conventions Base path: `/api/v1` Authentication: - Use JWT access token in header: `Authorization: Bearer ` - Login/refresh endpoints return `{ access_token, refresh_token, token_type }` Response codes: - `200` success - `201` created - `204` success, no body - `400` bad request - `401` unauthorized - `403` forbidden - `404` not found - `409` conflict - `422` validation/business rule error - `429` rate limit - `503` external service unavailable (email/storage/readiness) Common error body: ```json { "detail": "Error text" } ``` For `/health/ready` failure: ```json { "detail": { "status": "not_ready", "db": false, "redis": true } } ``` ## 2. Enums ### ChatType - `private` - `group` - `channel` ### ChatMemberRole - `owner` - `admin` - `member` ### MessageType - `text` - `image` - `video` - `audio` - `voice` - `file` - `circle_video` ### Message status events - `message_delivered` - `message_read` ## 3. Models (request/response) ## 3.1 Auth ### RegisterRequest ```json { "email": "user@example.com", "name": "Benya", "username": "benya", "password": "strongpassword" } ``` ### LoginRequest ```json { "email": "user@example.com", "password": "strongpassword", "otp_code": "123456", "recovery_code": "ABCDE-12345" } ``` `otp_code` is optional and used when 2FA is enabled. `recovery_code` is optional one-time fallback when 2FA is enabled. ### TokenResponse ```json { "access_token": "jwt", "refresh_token": "jwt", "token_type": "bearer" } ``` ### SessionRead ```json { "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 ```json { "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 ```json { "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 ```json { "id": 2, "name": "Other User", "username": "other", "email": "other@example.com", "avatar_url": null } ``` ### UserProfileUpdate ```json { "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 ```json { "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 ```json { "type": "group", "title": "My Group", "handle": "mygroup", "description": "optional", "is_public": true, "member_ids": [2, 3] } ``` Rules: - `private`: requires exactly 1 target in `member_ids` - `group`/`channel`: require `title` - `private` cannot be public - public chat requires `handle` ### ChatDetailRead `ChatRead + members[]`, where member item is: ```json { "id": 1, "user_id": 2, "role": "member", "joined_at": "2026-03-08T09:00:00Z" } ``` ## 3.4 Messages ### MessageRead ```json { "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 ```json { "chat_id": 10, "type": "text", "text": "Hello", "client_message_id": "client-msg-0001", "reply_to_message_id": null } ``` ### MessageForwardRequest ```json { "target_chat_id": 20, "include_author": true } ``` ### MessageForwardBulkRequest ```json { "target_chat_ids": [20, 21], "include_author": true } ``` ### MessageStatusUpdateRequest ```json { "chat_id": 10, "message_id": 100, "status": "message_read" } ``` ### MessageReactionToggleRequest ```json { "emoji": "👍" } ``` ## 3.5 Media ### UploadUrlRequest ```json { "file_name": "photo.jpg", "file_type": "image/jpeg", "file_size": 123456 } ``` ### UploadUrlResponse ```json { "upload_url": "https://...signed...", "file_url": "https://.../bucket/uploads/....jpg", "object_key": "uploads/....jpg", "expires_in": 900, "required_headers": { "Content-Type": "image/jpeg" } } ``` ### AttachmentCreateRequest ```json { "message_id": 100, "file_url": "https://.../bucket/uploads/....jpg", "file_type": "image/jpeg", "file_size": 123456, "waveform_points": [4, 7, 10, 9, 6] } ``` ### AttachmentRead ```json { "id": 1, "message_id": 100, "file_url": "https://...", "file_type": "image/jpeg", "file_size": 123456, "waveform_points": [4, 7, 10, 9, 6] } ``` ### ChatAttachmentRead ```json { "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 ```json { "users": [], "chats": [], "messages": [] } ``` ### NotificationRead ```json { "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: ```json { "status": "ok" } ``` ### GET `/health/live` Returns: ```json { "status": "ok" } ``` ### GET `/health/ready` Returns: ```json { "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: ```json { "refresh_token": "jwt" } ``` Response: `200` + `TokenResponse` ### POST `/api/v1/auth/verify-email` Body: ```json { "token": "verification_token" } ``` Response: `200` + `MessageResponse` ### POST `/api/v1/auth/resend-verification` Body: ```json { "email": "user@example.com" } ``` Response: `200` + `MessageResponse` ### POST `/api/v1/auth/request-password-reset` Body: ```json { "email": "user@example.com" } ``` Response: `200` + `MessageResponse` ### POST `/api/v1/auth/reset-password` Body: ```json { "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: ```json { "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: ```json { "code": "123456" } ``` Response: `200` + `MessageResponse` ### POST `/api/v1/auth/2fa/disable` Auth required. Body: ```json { "code": "123456" } ``` Response: `200` + `MessageResponse` ### POST `/api/v1/auth/2fa/recovery-codes/regenerate` Auth required. Body: ```json { "code": "123456" } ``` Response: ```json { "codes": ["ABCDE-12345", "FGHIJ-67890"] } ``` Codes are one-time and shown only at generation time. ### GET `/api/v1/auth/2fa/recovery-codes/status` Auth required. Response: ```json { "remaining_codes": 8 } ``` ## 6. Users endpoints ### GET `/api/v1/users/me` Auth required. Response: `200` + `UserRead` ### GET `/api/v1/users/search?query=&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: ```json { "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` (default `50`) - `before_id` (optional) - `query` (optional search by title/type) - `archived` (default `false`) 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=&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: ```json { "title": "New title" } ``` Response: `200` + `ChatRead` ### PATCH `/api/v1/chats/{chat_id}/profile` Auth required. Body (all fields optional): ```json { "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: ```json { "user_id": 123 } ``` Response: `201` + `ChatMemberRead` ### PATCH `/api/v1/chats/{chat_id}/members/{user_id}/role` Auth required. Body: ```json { "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: ```json { "message_id": 100 } ``` or unpin: ```json { "message_id": null } ``` Response: `200` + `ChatRead` ### GET `/api/v1/chats/{chat_id}/notifications` Auth required. Response: ```json { "chat_id": 10, "user_id": 1, "muted": false } ``` ### PUT `/api/v1/chats/{chat_id}/notifications` Auth required. Body: ```json { "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_avatar` controls whether other users receive `avatar_url`. - `privacy_last_seen` controls whether private-chat counterpart presence fields are visible: - `counterpart_is_online` - `counterpart_last_seen_at` - For `contacts` mode, 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 `contacts` can be added only by users present in their contacts list. - Applies to group/channel creation with initial members and admin add-member action. ### 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: ```json { "chat_id": 10, "token": "invite_token", "invite_url": "https://frontend/join?token=invite_token" } ``` ### POST `/api/v1/chats/join-by-invite` Auth required. Body: ```json { "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=&chat_id=&limit=50` Auth required. Response: `200` + `MessageRead[]` ### GET `/api/v1/messages/{chat_id}?limit=50&before_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: ```json { "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: ```json { "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: ```json [ { "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_url` must point to configured S3 bucket endpoint - Only message sender can attach files ### GET `/api/v1/media/chats/{chat_id}/attachments?limit=100&before_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=&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_MINUTE` for `/auth/login` - `REGISTER_RATE_LIMIT_PER_MINUTE` for `/auth/register` - `REFRESH_RATE_LIMIT_PER_MINUTE` for `/auth/refresh` - `RESET_RATE_LIMIT_PER_MINUTE` for reset/resend flows - `MESSAGE_RATE_LIMIT_PER_MINUTE` and `DUPLICATE_MESSAGE_COOLDOWN_SECONDS` for sending messages 429 response example: ```json { "detail": "Rate limit exceeded. Retry in 34 seconds." } ``` ## 13. Notes - `public_id` is 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. - `email` router exists in codebase but has no public REST endpoints yet.