The Chat WebSocket system enables real-time communication between users. The system provides:
ChatConsumer handles WebSocket connections
User → WebSocket Connection → JWT Auth → Participant Check → Join Group → Ready
↓
User sends message → Save to DB → Broadcast to Group → All participants receive
WebSocket connections require JWT authentication. The token must be provided in one of two ways:
ws://api.qsocial.net/ws/chat/{room_id}/?token={{jwt_token}}
Some WebSocket clients support custom headers:
Authorization: Bearer {{jwt_token}}
The room_id is the unique identifier (primary key) of a ChatRoom instance.
It's a numeric value that uniquely identifies a chat room between users. You need the room_id to:
room_id is returned as id in all chat room API responses.
When you create a direct chat, the response includes the room_id:
POST /api/chats/{
"user_id": 123
}
Response:
{
"id": 456, // ← This is the room_id
"participants": [
{"id": 1, "username": "current_user"},
{"id": 123, "username": "other_user"}
],
"is_group": false,
"created_at": "2024-03-20T10:00:00Z",
"unread_count": 0
}
Note: If a chat room already exists between you and the specified user, the existing room is returned (no new room is created).
Get all chat rooms you're part of. Each room in the list has an id field:
GET /api/chats/{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"id": 456, // ← room_id
"participants": [
{"id": 1, "username": "user1"},
{"id": 123, "username": "user2"}
],
"last_message": {
"id": 789,
"content": "Hello!",
"created_at": "2024-03-20T10:00:00Z"
},
"unread_count": 3,
"is_group": false
},
{
"id": 789, // ← another room_id
"participants": [...],
"last_message": Ellipsis,
"unread_count": 0,
"is_group": false
}
]
}
If you already know the room_id, you can fetch that room's details directly:
GET /api/chats/{room_id}/GET /api/chats/456/
Once you have the room_id, you can use it for various operations:
POST /api/chats/{room_id}/messages/POST /api/chats/456/messages/{
"content": "Hello, this is a test message!"
}
GET /api/chats/{room_id}/GET /api/chats/456/
POST /api/chats/{room_id}/mark-read/POST /api/chats/456/mark-read/
ws://api.qsocial.net/ws/chat/{room_id}/?token={{jwt_token}}ws://api.qsocial.net/ws/chat/456/?token=eyJ0eXAiOiJKV1QiLCJhbGc...
// Step 1: Create or get chat room
const response = await fetch('http://localhost:8000/api/chats/', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
user_id: 123
})
});
const room = await response.json();
const roomId = room.id; // Extract room_id
console.log('Room ID:', roomId);
// Step 2: Use room_id to send a message
await fetch(`http://localhost:8000/api/chats/${roomId}/messages/`, {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: 'Hello!'
})
});
// Step 3: Connect to WebSocket using room_id
const ws = new WebSocket(
`ws://localhost:8000/ws/chat/${roomId}/?token=${token}`
);
ws://api.qsocial.net/ws/chat/{room_id}/wss://api.qsocial.net/ws/chat/{room_id}/
| Code | Meaning | Description |
|---|---|---|
| 1000 | Normal Closure | Connection closed normally |
| 4001 | Unauthorized | User is not authenticated or token is invalid |
| 4003 | Forbidden | User is not a participant in this chat room |
| 1011 | Internal Error | Server error during connection |
All WebSocket messages are JSON objects with a type field that identifies the event type.
This section documents all events you can send (trigger) and all events you will receive (listen to).
These are events you send to the server via WebSocket:
chat_message{
"type": "chat_message",
"content": "Your message text here"
}
Payload to Send (With Attachment URL):
{
"type": "chat_message",
"content": "Check out this image!",
"attachment_url": "https://example.com/files/image.jpg",
"attachment_type": "image",
"attachment_name": "image.jpg"
}
Payload to Send (With Base64 Attachment):
{
"type": "chat_message",
"content": "Here's a small image",
"attachment_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"attachment_type": "image",
"attachment_name": "photo.jpg"
}
Field Descriptions:
type (required): Must be "chat_message"content (optional): The message text content. Can also use "message" as alternative field name. Required if no attachment is provided.attachment_url (optional): URL of an already uploaded attachment. Use this if you've uploaded the file via REST API first.attachment_base64 (optional): Base64 encoded file data. Use for smaller files (recommended max: 1MB).attachment_type (optional): Type of attachment - "image", "video", "audio", "document", or "file". Auto-detected if not provided.attachment_name (optional): Original filename of the attachment.content or an attachment (attachment_url or attachment_base64) is required. Empty messages without attachments are ignored.
attachment_urlattachment_base64 directlytyping{
"type": "typing"
}
Field Descriptions:
type (required): Must be "typing"read_receipt{
"type": "read_receipt",
"message_id": "123"
}
Field Descriptions:
type (required): Must be "read_receipt"message_id (required): The ID of the message to mark as read (string or number)These are events you receive from the server via WebSocket:
chat_message{
"type": "chat_message",
"content": "Message content",
"user_id": 7,
"username": "john_doe",
"full_name": "John Doe",
"message_id": "123",
"timestamp": "2025-11-15T18:23:21.483664+00:00"
}
Payload Received (With Attachment):
{
"type": "chat_message",
"content": "Check out this image!",
"user_id": 7,
"username": "john_doe",
"full_name": "John Doe",
"message_id": "123",
"timestamp": "2025-11-15T18:23:21.483664+00:00",
"attachment_url": "https://api.qsocial.net/media/chat/attachments/abc123.jpg",
"attachment_type": "image",
"attachment_name": "photo.jpg",
"attachment_size": 245678
}
Field Descriptions:
type: Always "chat_message"content: The message text content (may be empty if message only has attachment)user_id: ID of the user who sent the messageusername: Username of the senderfull_name: Full name of the sender (may be null)message_id: Unique ID of the message (string)timestamp: ISO 8601 timestamp when the message was createdattachment_url (optional): URL to download the attachment file. Present only if message has an attachment.attachment_type (optional): Type of attachment - "image", "video", "audio", "document", or "file". Present only if message has an attachment.attachment_name (optional): Original filename of the attachment. Present only if message has an attachment.attachment_size (optional): Size of the attachment in bytes. Present only if message has an attachment.attachment_url field to determine if message has an attachmentattachment_type to determine how to display the attachment (image viewer, video player, download link, etc.)attachment_url to display or save locallyattachment_size for user informationuser_online{
"type": "user_online",
"user_id": 7,
"username": "john_doe",
"full_name": "John Doe"
}
Field Descriptions:
type: Always "user_online"user_id: ID of the user who came onlineusername: Username of the userfull_name: Full name of the user (may be null)user_offline{
"type": "user_offline",
"user_id": 7,
"username": "john_doe"
}
Field Descriptions:
type: Always "user_offline"user_id: ID of the user who went offlineusername: Username of the useruser_typing{
"type": "user_typing",
"user_id": 7,
"username": "john_doe",
"full_name": "John Doe"
}
Field Descriptions:
type: Always "user_typing"user_id: ID of the user who is typingusername: Username of the userfull_name: Full name of the user (may be null)message_read{
"type": "message_read",
"message_id": "123",
"user_id": 7,
"username": "john_doe"
}
Field Descriptions:
type: Always "message_read"message_id: ID of the message that was marked as read (string)user_id: ID of the user who marked the message as readusername: Username of the usererror{
"type": "error",
"message": "Error processing message"
}
Field Descriptions:
type: Always "error"message: Human-readable error message"Invalid JSON" - The message you sent was not valid JSON"Error processing message" - Server-side error occurred while processing your message| Event Type | Direction | When | Who Receives |
|---|---|---|---|
chat_message |
Send → Receive | When you send a message | All other participants in the room |
typing |
Send | When you want to show typing indicator | N/A (triggers user_typing event) |
user_typing |
Receive | When another participant sends typing indicator | All other participants |
read_receipt |
Send | When you mark a message as read | N/A (triggers message_read event) |
message_read |
Receive | When another participant marks a message as read | All other participants |
user_online |
Receive | When a participant connects | All participants in the room |
user_offline |
Receive | When a participant disconnects | All participants in the room |
error |
Receive | When an error occurs processing your message | Only the sender of the problematic message |
/api/chats/type: Filter by type (direct, group)unread_only: Show only rooms with unread messagessearch: Search rooms by participant name/api/chats/{
"user_id": 123,
"post_id": 456 // Optional: if chat initiated from a post
}
/api/chats/{room_id}//api/chats/{room_id}/messages/{
"content": "Your message here"
}
Request Body (With Attachment - multipart/form-data):
Content-Type: multipart/form-data
content: "Check out this image!"
attachment: [file binary data]
content (optional): Text content of the message. Required if no attachment is provided.attachment (optional): File attachment. Can be image, video, document, or audio file. Required if no content is provided.{
"id": 123,
"sender": {
"id": 7,
"username": "john_doe",
"full_name": "John Doe"
},
"content": "Check out this image!",
"created_at": "2025-11-15T18:23:21.483664+00:00",
"is_read": false,
"attachment_url": "https://api.qsocial.net/media/chat/attachments/abc123.jpg",
"attachment_type": "image",
"attachment_name": "photo.jpg",
"attachment_size": 245678
}
multipart/form-data content typecontent and attachment if you want to send a message with a captioncontent or attachment must be provided. Empty messages without attachments will be rejected.
/api/chats/messages/{message_id}//api/chats/{room_id}/mark-read/| Error | Cause | Solution |
|---|---|---|
| Connection Refused | Server not running or wrong URL | Verify server is running and URL is correct |
| 401 Unauthorized | Invalid or expired JWT token | Refresh token and reconnect |
| 403 Forbidden | User not a participant | Verify user has access to chat room |
| 500 Internal Error | Server-side error | Check server logs, retry connection |
// Exponential backoff reconnection
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
function reconnect() {
if (reconnectAttempts >= maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
reconnectAttempts++;
setTimeout(() => {
console.log(`Reconnecting (attempt ${reconnectAttempts})...`);
connect();
}, delay);
}
class ChatService {
constructor(jwtToken, roomId) {
this.jwtToken = jwtToken;
this.roomId = roomId;
this.ws = null;
this.onMessageCallback = null;
this.onUserOnlineCallback = null;
this.onUserOfflineCallback = null;
}
connect() {
const url = `wss://api.qsocial.net/ws/chat/${this.roomId}/?token=${this.jwtToken}`;
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('Connected to chat');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onclose = (event) => {
console.log('Connection closed:', event.code, event.reason);
// Implement reconnection logic here
};
}
handleMessage(data) {
switch (data.type) {
case 'chat_message':
if (this.onMessageCallback) {
this.onMessageCallback(data);
}
break;
case 'user_online':
if (this.onUserOnlineCallback) {
this.onUserOnlineCallback(data);
}
break;
case 'user_offline':
if (this.onUserOfflineCallback) {
this.onUserOfflineCallback(data);
}
break;
case 'user_typing':
if (this.onUserTypingCallback) {
this.onUserTypingCallback(data);
}
break;
case 'message_read':
if (this.onMessageReadCallback) {
this.onMessageReadCallback(data);
}
break;
case 'error':
if (this.onErrorCallback) {
this.onErrorCallback(data);
}
break;
}
}
sendReadReceipt(messageId) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'read_receipt',
message_id: messageId
}));
}
}
sendMessage(content) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'chat_message',
content: content
}));
}
}
sendTyping() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'typing'
}));
}
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
// Usage
const chat = new ChatService('your_jwt_token', 123);
chat.onMessageCallback = (data) => {
console.log('New message:', data);
// Update UI
};
chat.connect();
Chat WebSocket Documentation
Last Updated: September 12, 2026
API Version: 1.0