Dispute Chat WebSocket Documentation

Table of Contents

1. Overview

The Dispute Chat WebSocket system enables real-time communication between users involved in a dispute, including transaction participants and assigned administrators. The system provides:

2. Architecture

2.1 Components

2.2 Flow Diagram

User → WebSocket Connection → JWT Auth → Participant Check → Join Group → Ready
                                                                    ↓
User sends message → Save to DB → Broadcast to Group → All participants receive
        

3. Authentication

WebSocket connections require JWT authentication. The token must be provided in one of two ways:

3.1 Query String (Recommended)

Connection URL:
ws://api.qsocial.net/ws/dispute/{dispute_id}/?token={{jwt_token}}

3.2 Authorization Header

Some WebSocket clients support custom headers:

Authorization: Bearer {{jwt_token}}
Note: Not all WebSocket implementations support custom headers. Using query string is more reliable across different platforms.

4. WebSocket Connection

4.1 Connection URL

WebSocket Endpoint:
ws://api.qsocial.net/ws/dispute/{dispute_id}/

Production (WSS):
wss://api.qsocial.net/ws/dispute/{dispute_id}/

4.2 Connection Requirements

4.3 Connection Status Codes

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

5. Message Protocol

5.1 Sending Messages

Text Message

{
    "type": "dispute_message",
    "content": "Your message text here",
    "message_type": "text"
}

Attachment Message

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
}

Typing Indicator

{
    "type": "typing"
}

Read Receipt

{
    "type": "read_receipt",
    "message_id": "123"
}

5.2 Receiving Messages

Text Message Event

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

User Online Event

{
    "type": "user_online",
    "user_id": 7,
    "username": "john_doe"
}

User Offline Event

{
    "type": "user_offline",
    "user_id": 7,
    "username": "john_doe"
}

User Typing Event

{
    "type": "user_typing",
    "user_id": 7,
    "username": "john_doe"
}

Message Read Event

{
    "type": "message_read",
    "message_id": "123",
    "user_id": 7,
    "username": "john_doe"
}

6. Mobile Integration

6.1 Flutter Implementation

Dependencies

dependencies:
  web_socket_channel: ^2.4.0
  jwt_decoder: ^2.0.1

Connection Setup

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;
  }
}

6.2 React Native Implementation

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();
  }
}

6.3 iOS (Swift) Implementation

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()
  }
}

6.4 Android (Kotlin) Implementation

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")
  }
}

7. Admin Integration

7.1 Admin Access

Administrators can join any dispute they are assigned to. The system automatically:

7.2 Admin Web Interface

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'
}));

7.3 Admin-Specific Features

8. Error Handling

8.1 Connection Errors

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

8.2 Message Errors

Empty Content: Messages with empty content are ignored. Always validate content before sending.
Closed Disputes: Messages cannot be sent to closed disputes. Check dispute status before sending.

8.3 Reconnection Strategy

// 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);
}

9. REST API Endpoints

9.1 List Disputes

GET /api/disputes/
Returns list of disputes for authenticated user

9.2 Get Dispute Details

GET /api/disputes/{dispute_id}/
Returns detailed information about a specific dispute

9.3 Create Dispute

POST /api/disputes/
Body:
{
  "transaction_id": 123,
  "reason": "Product not received"
}

9.4 Send Message (HTTP)

POST /api/disputes/{dispute_id}/messages/
Body:
{
  "content": "Message text",
  "message_type": "text"
}
Note: While HTTP endpoint exists, WebSocket is recommended for real-time messaging.

9.5 Mark Messages Read

POST /api/disputes/{dispute_id}/mark-read/

9.6 Resolve Dispute

POST /api/disputes/{dispute_id}/resolve/
Body:
{
  "resolution": "resolved_in_favor_of_sender",
  "notes": "Resolution notes"
}

10. Code Examples

10.1 Complete Flutter Example

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> _messages = [];

  @override
  void initState() {
    super.initState();
    _connect();
  }

  void _connect() {
    final uri = Uri.parse(
      'wss://api.qsocial.net/ws/dispute/${widget.disputeId}/?token=${widget.jwtToken}'
    );

    channel = WebSocketChannel.connect(uri);

    channel.stream.listen(
      (message) {
        final data = json.decode(message);
        setState(() {
          if (data['type'] == 'dispute_message') {
            _messages.add(data);
          }
        });
      },
      onError: (error) {
        print('Error: $error');
      },
    );
  }

  void _sendMessage() {
    if (_messageController.text.isEmpty) return;

    channel.sink.add(json.encode({
      'type': 'dispute_message',
      'content': _messageController.text,
      'message_type': 'text'
    }));

    _messageController.clear();
  }

  @override
  void dispose() {
    channel.sink.close();
    _messageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Dispute Chat')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (context, index) {
                final msg = _messages[index];
                return ListTile(
                  title: Text(msg['username'] ?? 'Unknown'),
                  subtitle: Text(msg['content'] ?? ''),
                  trailing: Text(
                    msg['timestamp'] ?? '',
                    style: TextStyle(fontSize: 12),
                  ),
                );
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: InputDecoration(
                      hintText: 'Type a message...',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.send),
                  onPressed: _sendMessage,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

10.2 Complete JavaScript Example

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();

Best Practices

Security Considerations


Dispute Chat WebSocket Documentation
Last Updated: September 12, 2026
API Version: 1.0