The Dispute Chat WebSocket system enables real-time communication between users involved in a dispute, including transaction participants and assigned administrators. The system provides:
DisputeChatConsumer 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/dispute/{dispute_id}/?token={{jwt_token}}
Some WebSocket clients support custom headers:
Authorization: Bearer {{jwt_token}}
ws://api.qsocial.net/ws/dispute/{dispute_id}/wss://api.qsocial.net/ws/dispute/{dispute_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 dispute |
| 1011 | Internal Error | Server error during connection |
{
"type": "dispute_message",
"content": "Your message text here",
"message_type": "text"
}
For attachments, first upload via HTTP, then reference via WebSocket:
{
"type": "attachment_message",
"content": "Optional caption",
"attachment_url": "https://api.qsocial.net/media/disputes/attachment.jpg",
"attachment_id": 123
}
{
"type": "typing"
}
{
"type": "read_receipt",
"message_id": "123"
}
{
"type": "dispute_message",
"content": "Message content",
"message_type": "text",
"user_id": 7,
"username": "john_doe",
"message_id": "123",
"timestamp": "2025-11-15T18:23:21.483664+00:00"
}
{
"type": "user_online",
"user_id": 7,
"username": "john_doe"
}
{
"type": "user_offline",
"user_id": 7,
"username": "john_doe"
}
{
"type": "user_typing",
"user_id": 7,
"username": "john_doe"
}
{
"type": "message_read",
"message_id": "123",
"user_id": 7,
"username": "john_doe"
}
dependencies:
web_socket_channel: ^2.4.0
jwt_decoder: ^2.0.1
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/io.dart';
class DisputeChatService {
WebSocketChannel? _channel;
String? _jwtToken;
int? _disputeId;
Future connect(String jwtToken, int disputeId) async {
_jwtToken = jwtToken;
_disputeId = disputeId;
final uri = Uri.parse(
'wss://api.qsocial.net/ws/dispute/$disputeId/?token=$jwtToken'
);
try {
_channel = IOWebSocketChannel.connect(uri);
// Listen for messages
_channel!.stream.listen(
(message) {
_handleMessage(message);
},
onError: (error) {
print('WebSocket error: $error');
_handleError(error);
},
onDone: () {
print('WebSocket closed');
_handleDisconnect();
},
);
} catch (e) {
print('Connection error: $e');
throw Exception('Failed to connect to dispute chat');
}
}
void _handleMessage(dynamic message) {
final data = json.decode(message);
final type = data['type'];
switch (type) {
case 'dispute_message':
_onMessageReceived(data);
break;
case 'user_online':
_onUserOnline(data);
break;
case 'user_offline':
_onUserOffline(data);
break;
case 'user_typing':
_onUserTyping(data);
break;
case 'message_read':
_onMessageRead(data);
break;
}
}
void sendMessage(String content) {
if (_channel == null) return;
final message = {
'type': 'dispute_message',
'content': content,
'message_type': 'text'
};
_channel!.sink.add(json.encode(message));
}
void sendTyping() {
if (_channel == null) return;
_channel!.sink.add(json.encode({
'type': 'typing'
}));
}
void disconnect() {
_channel?.sink.close();
_channel = null;
}
}
import io from 'socket.io-client';
class DisputeChatService {
constructor(jwtToken, disputeId) {
this.socket = io('wss://api.qsocial.net', {
path: '/ws/dispute/' + disputeId + '/',
query: { token: jwtToken },
transports: ['websocket'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5
});
this.setupListeners();
}
setupListeners() {
this.socket.on('connect', () => {
console.log('Connected to dispute chat');
});
this.socket.on('dispute_message', (data) => {
this.onMessageReceived(data);
});
this.socket.on('user_online', (data) => {
this.onUserOnline(data);
});
this.socket.on('user_offline', (data) => {
this.onUserOffline(data);
});
this.socket.on('error', (error) => {
console.error('WebSocket error:', error);
});
}
sendMessage(content) {
this.socket.emit('dispute_message', {
type: 'dispute_message',
content: content,
message_type: 'text'
});
}
disconnect() {
this.socket.disconnect();
}
}
import Foundation
import Starscream
class DisputeChatService: WebSocketDelegate {
var socket: WebSocket?
let jwtToken: String
let disputeId: Int
init(jwtToken: String, disputeId: Int) {
self.jwtToken = jwtToken
self.disputeId = disputeId
}
func connect() {
var request = URLRequest(url: URL(string:
"wss://api.qsocial.net/ws/dispute/\(disputeId)/?token=\(jwtToken)")!)
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket?.delegate = self
socket?.connect()
}
func didReceive(event: WebSocketEvent, client: WebSocketClient) {
switch event {
case .connected(let headers):
print("Connected: \(headers)")
case .disconnected(let reason, let code):
print("Disconnected: \(reason) with code: \(code)")
case .text(let string):
handleMessage(string)
case .error(let error):
print("Error: \(error?.localizedDescription ?? "Unknown")")
default:
break
}
}
func sendMessage(content: String) {
let message: [String: Any] = [
"type": "dispute_message",
"content": content,
"message_type": "text"
]
if let jsonData = try? JSONSerialization.data(withJSONObject: message),
let jsonString = String(data: jsonData, encoding: .utf8) {
socket?.write(string: jsonString)
}
}
func disconnect() {
socket?.disconnect()
}
}
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.json.JSONObject
class DisputeChatService(
private val jwtToken: String,
private val disputeId: Int
) {
private var webSocket: WebSocket? = null
private val client = OkHttpClient()
fun connect() {
val request = Request.Builder()
.url("wss://api.qsocial.net/ws/dispute/$disputeId/?token=$jwtToken")
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
println("Connected to dispute chat")
}
override fun onMessage(webSocket: WebSocket, text: String) {
handleMessage(text)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
println("Connection failed: ${t.message}")
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(1000, null)
}
})
}
fun sendMessage(content: String) {
val message = JSONObject().apply {
put("type", "dispute_message")
put("content", content)
put("message_type", "text")
}
webSocket?.send(message.toString())
}
fun disconnect() {
webSocket?.close(1000, "User disconnected")
}
}
Administrators can join any dispute they are assigned to. The system automatically:
For web-based admin panels, use the same WebSocket connection:
// JavaScript/TypeScript
const ws = new WebSocket(
`wss://api.qsocial.net/ws/dispute/${disputeId}/?token=${adminToken}`
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Handle message
};
ws.send(JSON.stringify({
type: 'dispute_message',
content: 'Admin message',
message_type: 'text'
}));
| 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 dispute |
| 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);
}
/api/disputes//api/disputes/{dispute_id}//api/disputes/{
"transaction_id": 123,
"reason": "Product not received"
}
/api/disputes/{dispute_id}/messages/{
"content": "Message text",
"message_type": "text"
}
/api/disputes/{dispute_id}/mark-read/
/api/disputes/{dispute_id}/resolve/{
"resolution": "resolved_in_favor_of_sender",
"notes": "Resolution notes"
}
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';
class DisputeChatScreen extends StatefulWidget {
final String jwtToken;
final int disputeId;
const DisputeChatScreen({
required this.jwtToken,
required this.disputeId,
});
@override
_DisputeChatScreenState createState() => _DisputeChatScreenState();
}
class _DisputeChatScreenState extends State {
late WebSocketChannel channel;
final TextEditingController _messageController = TextEditingController();
final List
class DisputeChat {
constructor(jwtToken, disputeId) {
this.jwtToken = jwtToken;
this.disputeId = disputeId;
this.ws = null;
this.onMessageCallback = null;
this.onUserOnlineCallback = null;
this.onUserOfflineCallback = null;
}
connect() {
const url = `wss://api.qsocial.net/ws/dispute/${this.disputeId}/?token=${this.jwtToken}`;
this.ws = new WebSocket(url);
this.ws.onopen = () => {
console.log('Connected to dispute 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 'dispute_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;
}
}
sendMessage(content) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({
type: 'dispute_message',
content: content,
message_type: 'text'
}));
}
}
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 DisputeChat('your_jwt_token', 123);
chat.onMessageCallback = (data) => {
console.log('New message:', data);
// Update UI
};
chat.connect();
Dispute Chat WebSocket Documentation
Last Updated: September 12, 2026
API Version: 1.0