Version: 1.0.0
Base URL: http://localhost:3000/api/v1 (Development)
WebSocket URL: ws://localhost:3000 (Development)
- Authentication
- User Management
- Friendship Management
- Messaging
- Encryption
- WebSocket Events
- Error Codes
- Rate Limiting
Sign in with Google OAuth (Firebase ID Token).
Request:
{
"idToken": "firebase-id-token",
"deviceInfo": {
"deviceName": "iPhone 15 Pro",
"deviceType": "ios",
"deviceToken": "apns-token-optional"
}
}Response (200):
{
"success": true,
"data": {
"accessToken": "jwt-access-token",
"refreshToken": "jwt-refresh-token",
"accessExpiresAt": "2026-01-09T12:00:00.000Z",
"refreshExpiresAt": "2026-01-16T12:00:00.000Z",
"user": {
"id": "uuid",
"firebaseUid": "firebase-uid",
"email": "user@example.com",
"username": "username",
"name": "User Name",
"profilePictureUrl": "https://...",
"bio": null,
"status": "offline",
"isProfileComplete": true
},
"device": {
"id": "device-uuid",
"deviceName": "iPhone 15 Pro",
"deviceType": "ios"
}
}
}Complete user profile after first login.
Headers:
Authorization: Bearer <access-token>
Request:
{
"username": "johndoe",
"phoneNumber": "+1234567890",
"age": 25
}Response (200):
{
"success": true,
"data": {
"user": {
"id": "uuid",
"username": "johndoe",
"phoneNumber": "+1234567890",
"age": 25,
...
}
}
}Refresh access token using refresh token.
Request:
{
"refreshToken": "jwt-refresh-token"
}Response (200):
{
"success": true,
"data": {
"accessToken": "new-jwt-access-token",
"refreshToken": "new-jwt-refresh-token",
"accessExpiresAt": "2026-01-09T12:15:00.000Z",
"refreshExpiresAt": "2026-01-16T12:00:00.000Z"
}
}Logout current session.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Logged out successfully"
}Get current user's profile.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"user": {
"id": "uuid",
"firebaseUid": "firebase-uid",
"username": "johndoe",
"email": "user@example.com",
"phoneNumber": "+1234567890",
"name": "John Doe",
"age": 25,
"profilePictureUrl": "https://...",
"bio": "Bio text",
"status": "online",
"lastSeen": "2026-01-09T12:00:00.000Z",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-09T12:00:00.000Z"
}
}
}Update current user's profile.
Headers:
Authorization: Bearer <access-token>
Request:
{
"name": "John Doe Updated",
"bio": "Updated bio",
"profilePictureUrl": "https://...",
"username": "newusername",
"phoneNumber": "+1234567890"
}Response (200):
{
"success": true,
"data": {
"user": {
"id": "uuid",
"name": "John Doe Updated",
...
}
}
}Get public profile of another user.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"user": {
"id": "uuid",
"username": "johndoe",
"name": "John Doe",
"profilePictureUrl": "https://...",
"bio": "Bio text",
"status": "online",
"lastSeen": "2026-01-09T12:00:00.000Z"
}
}
}Search users by username, email, or phone.
Headers:
Authorization: Bearer <access-token>
Query Parameters:
q(required): Search querytype(optional):username|email|phone|all(default:all)limit(optional): Number of results (default: 20, max: 100)offset(optional): Pagination offset (default: 0)
Response (200):
{
"success": true,
"data": {
"users": [
{
"id": "uuid",
"username": "johndoe",
"name": "John Doe",
"profilePictureUrl": "https://...",
"bio": "Bio text",
"status": "online"
}
],
"pagination": {
"total": 50,
"limit": 20,
"offset": 0,
"hasMore": true
}
}
}Update user's online status.
Headers:
Authorization: Bearer <access-token>
Request:
{
"status": "online" | "away" | "offline"
}Response (200):
{
"success": true,
"data": {
"status": "online",
"lastSeen": "2026-01-09T12:00:00.000Z"
}
}Delete user account (soft delete).
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Account deleted successfully"
}List all friends/friend requests.
Headers:
Authorization: Bearer <access-token>
Query Parameters:
status(optional):accepted|pending|denied|blockedlimit(optional): Number of results (default: 50, max: 100)offset(optional): Pagination offset (default: 0)
Response (200):
{
"success": true,
"data": {
"friendships": [
{
"id": "friendship-uuid",
"status": "accepted",
"requestedAt": "2026-01-01T00:00:00.000Z",
"respondedAt": "2026-01-01T00:05:00.000Z",
"isRequester": true,
"user": {
"id": "uuid",
"username": "johndoe",
"name": "John Doe",
"profilePictureUrl": "https://...",
"bio": "Bio text",
"status": "online"
}
}
],
"pagination": {
"total": 10,
"limit": 50,
"offset": 0,
"hasMore": false
}
}
}Send friend request.
Headers:
Authorization: Bearer <access-token>
Request:
{
"userId": "recipient-user-uuid"
}Response (201):
{
"success": true,
"data": {
"friendship": {
"id": "friendship-uuid",
"status": "pending",
"requestedAt": "2026-01-09T12:00:00.000Z"
}
}
}Get pending friend requests (sent and received).
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"sent": [
{
"id": "friendship-uuid",
"status": "pending",
"requestedAt": "2026-01-09T12:00:00.000Z",
"user": { ... }
}
],
"received": [
{
"id": "friendship-uuid",
"status": "pending",
"requestedAt": "2026-01-09T12:00:00.000Z",
"user": { ... }
}
]
}
}Accept a friend request.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"friendship": {
"id": "friendship-uuid",
"status": "accepted",
"respondedAt": "2026-01-09T12:05:00.000Z"
}
}
}Deny a friend request.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"friendship": {
"id": "friendship-uuid",
"status": "denied",
"respondedAt": "2026-01-09T12:05:00.000Z"
}
}
}Unfriend or cancel friend request.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Friendship deleted successfully"
}Block a user.
Headers:
Authorization: Bearer <access-token>
Request:
{
"reason": "Optional reason for blocking"
}Response (201):
{
"success": true,
"data": {
"block": {
"id": "block-uuid",
"blockerId": "your-uuid",
"blockedId": "blocked-user-uuid",
"reason": "Optional reason",
"createdAt": "2026-01-09T12:00:00.000Z"
}
}
}Unblock a user.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "User unblocked successfully"
}Send a message.
Headers:
Authorization: Bearer <access-token>
Request:
{
"recipientId": "recipient-uuid",
"encryptedContent": "base64-encoded-encrypted-content",
"encryptedKey": "base64-encoded-encryption-key",
"messageType": "text" | "image" | "video" | "audio" | "file" | "system",
"mediaUrl": "https://...",
"mediaSizeBytes": 1024000,
"replyToMessageId": "message-uuid"
}Response (201):
{
"success": true,
"data": {
"message": {
"id": "message-uuid",
"senderId": "your-uuid",
"recipientId": "recipient-uuid",
"messageType": "text",
"status": "sent",
"sentAt": "2026-01-09T12:00:00.000Z",
"replyToMessageId": null
}
}
}Get all conversations for current user.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"conversations": [
{
"userId": "other-user-uuid",
"username": "johndoe",
"name": "John Doe",
"profilePictureUrl": "https://...",
"lastMessage": {
"id": "message-uuid",
"messageType": "text",
"sentAt": "2026-01-09T12:00:00.000Z",
"status": "read"
},
"unreadCount": 5
}
]
}
}Get conversation between current user and another user.
Headers:
Authorization: Bearer <access-token>
Query Parameters:
limit(optional): Number of messages (default: 50, max: 100)offset(optional): Pagination offset (default: 0)beforeMessageId(optional): Get messages before this message ID
Response (200):
{
"success": true,
"data": {
"messages": [
{
"id": "message-uuid",
"senderId": "sender-uuid",
"recipientId": "recipient-uuid",
"messageType": "text",
"mediaUrl": null,
"mediaSizeBytes": null,
"status": "read",
"sentAt": "2026-01-09T12:00:00.000Z",
"deliveredAt": "2026-01-09T12:00:01.000Z",
"readAt": "2026-01-09T12:00:05.000Z",
"replyToMessageId": null,
"isEdited": false,
"editedAt": null,
"sender": {
"id": "sender-uuid",
"username": "johndoe",
"name": "John Doe",
"profilePictureUrl": "https://..."
},
"recipient": { ... }
}
],
"pagination": {
"total": 100,
"limit": 50,
"offset": 0,
"hasMore": true
}
}
}Edit a message (within 15 minutes).
Headers:
Authorization: Bearer <access-token>
Request:
{
"encryptedContent": "base64-encoded-new-encrypted-content",
"encryptedKey": "base64-encoded-new-encryption-key"
}Response (200):
{
"success": true,
"data": {
"message": {
"id": "message-uuid",
"isEdited": true,
"editedAt": "2026-01-09T12:10:00.000Z",
...
}
}
}Delete a message (soft delete).
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Message deleted successfully"
}Mark message as delivered.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Message marked as delivered"
}Mark message as read.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Message marked as read"
}Mark multiple messages as read.
Headers:
Authorization: Bearer <access-token>
Request:
{
"messageIds": ["message-uuid-1", "message-uuid-2"],
"senderId": "sender-uuid"
}Response (200):
{
"success": true,
"message": "Messages marked as read"
}Get unread message count.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"total": 15,
"perConversation": {
"user-uuid-1": 5,
"user-uuid-2": 10
}
}
}Initialize encryption keys for current device.
Headers:
Authorization: Bearer <access-token>
Response (201):
{
"success": true,
"data": {
"keys": {
"identityKeyPublic": "base64-encoded-key",
"signedPreKeyPublic": "base64-encoded-key",
"signedPreKeySignature": "base64-encoded-signature",
"prekeyCount": 100
}
}
}Get prekey bundle for X3DH key exchange.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"bundle": {
"identityKey": "base64-encoded-key",
"signedPreKey": {
"keyId": 1234567890,
"publicKey": "base64-encoded-key",
"signature": "base64-encoded-signature"
},
"oneTimePreKey": {
"keyId": 1234567891,
"publicKey": "base64-encoded-key"
}
}
}
}Establish encryption session with another user.
Headers:
Authorization: Bearer <access-token>
Request:
{
"recipientUserId": "recipient-uuid",
"recipientDeviceId": "recipient-device-uuid"
}Response (201):
{
"success": true,
"data": {
"session": {
"sessionId": "session-id",
"rootKey": "base64-encoded-key",
"sendingChainKey": "base64-encoded-key",
"receivingChainKey": "base64-encoded-key"
}
}
}Rotate encryption keys for current device.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"keys": {
"identityKeyPublic": "base64-encoded-key",
"signedPreKeyPublic": "base64-encoded-key",
"signedPreKeySignature": "base64-encoded-signature",
"prekeyCount": 100
}
}
}Get all encryption keys for current user.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"data": {
"keys": [
{
"id": "key-uuid",
"deviceId": "device-uuid",
"prekeyCount": 100,
"keyCreatedAt": "2026-01-09T12:00:00.000Z",
"keyExpiresAt": null,
"isActive": true
}
]
}
}Deactivate encryption keys for a device.
Headers:
Authorization: Bearer <access-token>
Response (200):
{
"success": true,
"message": "Encryption keys deactivated successfully"
}Connect to WebSocket server:
const socket = io('http://localhost:3000', {
auth: {
token: 'your-access-token'
}
});Send a message via WebSocket.
socket.emit('message:send', {
recipientId: 'recipient-uuid',
encryptedContent: 'base64-encoded-content',
encryptedKey: 'base64-encoded-key',
messageType: 'text',
mediaUrl: 'https://...',
mediaSizeBytes: 1024000,
replyToMessageId: 'message-uuid'
});Indicate user started typing.
socket.emit('typing:start', {
recipientId: 'recipient-uuid'
});Indicate user stopped typing.
socket.emit('typing:stop', {
recipientId: 'recipient-uuid'
});Mark message as read.
socket.emit('message:read', {
messageId: 'message-uuid'
});Mark multiple messages as read.
socket.emit('messages:read', {
messageIds: ['message-uuid-1', 'message-uuid-2'],
senderId: 'sender-uuid'
});Update user status.
socket.emit('status:update', {
status: 'online' | 'away' | 'offline'
});Confirmation that message was sent.
socket.on('message:sent', (data) => {
console.log('Message sent:', data.message);
});New message received.
socket.on('message:received', (data) => {
console.log('New message:', data.message);
});Message was delivered to recipient.
socket.on('message:delivered', (data) => {
console.log('Message delivered:', data.messageId);
});Message was read by recipient.
socket.on('message:read', (data) => {
console.log('Message read:', data.messageId);
});Error sending message.
socket.on('message:error', (data) => {
console.error('Message error:', data.error);
});User started typing.
socket.on('typing:start', (data) => {
console.log(`${data.name} is typing...`);
});User stopped typing.
socket.on('typing:stop', (data) => {
console.log(`${data.userId} stopped typing`);
});User came online.
socket.on('user:online', (data) => {
console.log(`${data.name} is now online`);
});User went offline.
socket.on('user:offline', (data) => {
console.log(`${data.name} is now offline`);
});User status changed.
socket.on('user:status', (data) => {
console.log(`User ${data.userId} status: ${data.status}`);
});All error responses follow this format:
{
"success": false,
"error": {
"message": "Error message",
"code": "ERROR_CODE",
"details": []
},
"timestamp": "2026-01-09T12:00:00.000Z"
}| Code | HTTP Status | Description |
|---|---|---|
AUTH_REQUIRED |
401 | Authentication token required |
AUTH_FAILED |
401 | Authentication failed |
TOKEN_EXPIRED |
401 | Access token expired |
SESSION_INVALID |
401 | Session not found or inactive |
USER_INVALID |
401 | User not found or inactive |
VALIDATION_ERROR |
400 | Request validation failed |
USER_NOT_FOUND |
404 | User not found |
MESSAGE_NOT_FOUND |
404 | Message not found |
FRIENDSHIP_NOT_FOUND |
404 | Friendship not found |
USERNAME_TAKEN |
409 | Username already taken |
PHONE_TAKEN |
409 | Phone number already registered |
RATE_LIMIT_EXCEEDED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Internal server error |
- Window: 15 minutes
- Limit: 100 requests per window per IP
- Headers: Rate limit info included in response headers:
X-RateLimit-Limit: Maximum requestsX-RateLimit-Remaining: Remaining requestsX-RateLimit-Reset: Reset time (Unix timestamp)
-
Authentication:
- All protected endpoints require
Authorization: Bearer <access-token>header - Access tokens expire in 15 minutes
- Use refresh token to get new access token
- Store tokens securely (Keychain on iOS)
- All protected endpoints require
-
WebSocket:
- Connect with access token in
auth.token - Reconnect automatically on disconnect
- Handle connection errors gracefully
- Connect with access token in
-
Encryption:
- Initialize keys on first app launch
- Store private keys securely (never send to server)
- Perform X3DH key exchange before first message
- Implement Double Ratchet on client side
-
Pagination:
- Use
limitandoffsetfor pagination - Check
hasMorein pagination object - Use
beforeMessageIdfor message pagination
- Use
-
Error Handling:
- Always check
successfield in response - Handle
401errors by refreshing token - Show user-friendly error messages
- Log errors for debugging
- Always check
-
Real-time Updates:
- Use WebSocket for real-time features
- Fallback to polling if WebSocket fails
- Handle offline/online state changes
Last Updated: January 9, 2026
API Version: v1