Files
Messenger/docs/api-reference.md
benya 90c2bdcd96
Some checks are pending
CI / test (push) Has started running
docs(api): document owner-only chat role update rules
2026-03-08 20:18:18 +03:00

21 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:

  • 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:

{
  "detail": "Error text"
}

For /health/ready failure:

{
  "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

Realtime chat activity events

  • typing_start
  • typing_stop
  • recording_voice_start
  • recording_voice_stop
  • recording_video_start
  • recording_video_stop

Server behavior: when a user disconnects, active typing/recording indicators are auto-cleared with corresponding *_stop events.

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",
  "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

{
  "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 in member_ids
  • group/channel: require title
  • private cannot 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
Behavior: revokes only the specified refresh session token (jti); current access token remains valid.

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

POST /api/v1/auth/2fa/recovery-codes/regenerate

Auth required. Body:

{ "code": "123456" }

Response:

{
  "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:

{
  "remaining_codes": 8
}

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 (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=<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

Rules:

  • only owner can change member roles.
  • owner cannot demote self from owner.

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

Behavior:

  • saved messages: clears personal history only (chat is not removed).
  • channel + role owner/admin: deletes channel for all members.
  • channel + role member: acts as leave channel (removes only current membership).
  • group/private with for_all=false: removes chat for current user only.
  • for_all=true (where allowed): deletes chat globally.

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_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:

{
  "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

Behavior:

  • for_all=false:
    • in regular chats: hides message only for current user.
    • in channels: not allowed (422), channel messages are delete-for-all only.
  • for_all=true:
    • private chats: allowed.
    • own messages: allowed.
    • group/channel messages by others: allowed only for owner/admin.

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_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=<id>

Auth required.
Response: 200 + ChatAttachmentRead[]

10. Notifications

GET /api/v1/notifications?limit=50

Auth required.
Response: 200 + NotificationRead[]

11. Realtime WebSocket

Endpoint

GET /api/v1/realtime/ws?token=<access_jwt> (WebSocket upgrade)

Auth:

  • token is required in query string
  • invalid/expired token: socket closed with policy violation

Client -> Server events

{ "event": "ping", "payload": {} }
{
  "event": "send_message",
  "payload": {
    "chat_id": 10,
    "type": "text",
    "text": "Hello",
    "client_message_id": "uuid",
    "reply_to_message_id": null
  }
}
{ "event": "typing_start", "payload": { "chat_id": 10 } }
{ "event": "typing_stop", "payload": { "chat_id": 10 } }
{ "event": "recording_voice_start", "payload": { "chat_id": 10 } }
{ "event": "recording_voice_stop", "payload": { "chat_id": 10 } }
{ "event": "recording_video_start", "payload": { "chat_id": 10 } }
{ "event": "recording_video_stop", "payload": { "chat_id": 10 } }
{ "event": "message_delivered", "payload": { "chat_id": 10, "message_id": 123 } }
{ "event": "message_read", "payload": { "chat_id": 10, "message_id": 123 } }

Server -> Client envelope

{
  "event": "receive_message",
  "payload": {},
  "timestamp": "2026-03-08T10:00:00Z"
}

Common server events:

  • connect, pong, error
  • receive_message, message_updated, message_deleted
  • typing_start, typing_stop
  • recording_voice_start, recording_voice_stop
  • recording_video_start, recording_video_stop
  • message_delivered, message_read
  • user_online, user_offline
  • chat_updated, chat_deleted

Notes:

  • on disconnect, server auto-emits activity stop events (typing_stop, recording_*_stop) for active chat indicators.
  • after revoke-all sessions, realtime connections are force-closed; web client should treat this as logout.

GET /api/v1/search?query=<text>&users_limit=10&chats_limit=10&messages_limit=10

Auth required.
Response: 200 + GlobalSearchRead

13. 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:

{
  "detail": "Rate limit exceeded. Retry in 34 seconds."
}

14. 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.