WebSocket
The WebSocket protocol enables real-time bidirectional messaging. Use the REST API to get a connection token, then connect via WebSocket for live message updates.
Getting a Token
Before connecting, request a WebSocket token via the REST API:
POST /api/v1/ws-tokencURL Example
curl -X POST https://api.deliverychat.online/api/v1/ws-token \
-H "Authorization: Bearer dk_live_your_api_key" \
-H "X-App-Id: your-app-id" \
-H "X-Visitor-Id: 550e8400-e29b-41d4-a716-446655440000" \
-H "Origin: https://your-domain.com"Response — 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}The token encodes your appId, visitorId, and request origin. It is used to authenticate the WebSocket connection.
Connecting
Open a WebSocket connection using the token as a query parameter:
wss://api.deliverychat.online/v1/ws?token=eyJhbGci...JavaScript Example
const ws = new WebSocket(
`wss://api.deliverychat.online/v1/ws?token=${token}`
);
ws.onopen = () => {
console.log("Connected");
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Event:", data.type, data.payload);
};
ws.onclose = (event) => {
console.log("Disconnected:", event.code, event.reason);
};Why a query parameter?
WebSocket does not support custom headers during the handshake in browsers. The token is passed as a query parameter instead. The app ID is embedded in the token — you do not need to pass it separately.
Client Events (Send)
Events you send to the server:
room:join
Join a conversation room to receive messages.
{
"type": "room:join",
"payload": {
"conversationId": "conv-uuid",
"lastMessageId": "msg-uuid"
}
}| Field | Type | Required | Description |
|---|---|---|---|
conversationId | string (UUID) | Yes | Conversation to join |
lastMessageId | string (UUID) | No | Last message the client has — triggers messages:sync for missed messages |
room:leave
Leave a conversation room.
{
"type": "room:leave",
"payload": {
"conversationId": "conv-uuid"
}
}message:send
Send a message to a conversation.
{
"type": "message:send",
"payload": {
"conversationId": "conv-uuid",
"content": "Hello!",
"contentFormat": "plain",
"clientMessageId": "client-generated-uuid"
}
}| Field | Type | Required | Description |
|---|---|---|---|
conversationId | string (UUID) | Yes | Target conversation |
content | string | Yes | Message text (max 10,000 chars). For rich text, this is a Lexical JSON string. |
contentFormat | string | No | "plain" (default) or "lexical". See Message Formats. |
clientMessageId | string | Yes | Client-generated ID for optimistic UI matching |
typing:start
Notify other participants that you are typing.
{
"type": "typing:start",
"payload": {
"conversationId": "conv-uuid"
}
}typing:stop
Notify other participants that you stopped typing.
{
"type": "typing:stop",
"payload": {
"conversationId": "conv-uuid"
}
}ping
Heartbeat to keep the connection alive.
{
"type": "ping"
}Server Events (Receive)
Events the server sends to your client:
message:new
A new message in a room you’ve joined. Sent to all participants except the sender.
{
"type": "message:new",
"payload": {
"id": "msg-uuid",
"conversationId": "conv-uuid",
"senderId": "user-uuid",
"senderName": "Support Agent",
"senderRole": "operator",
"content": "How can I help you?",
"contentFormat": "plain",
"contentHtml": null,
"type": "text",
"createdAt": "2025-01-15T10:31:00.000Z"
}
}message:ack
Confirmation that your message was persisted. Sent only to the sender.
{
"type": "message:ack",
"payload": {
"clientMessageId": "client-generated-uuid",
"serverMessageId": "msg-uuid",
"createdAt": "2025-01-15T10:30:05.000Z"
}
}Use clientMessageId to match the ACK to your optimistic UI update and replace it with the server-confirmed data.
message:edited
A message in a room you’ve joined was edited. Update your local message with the new content.
{
"type": "message:edited",
"payload": {
"conversationId": "conv-uuid",
"messageId": "msg-uuid",
"content": "Updated message content",
"contentFormat": "plain",
"contentHtml": null,
"editedAt": "2025-01-15T10:32:00.000Z",
"senderId": "user-uuid"
}
}message:deleted
A message in a room you’ve joined was soft-deleted. Remove it from your UI.
{
"type": "message:deleted",
"payload": {
"conversationId": "conv-uuid",
"messageId": "msg-uuid",
"senderId": "user-uuid"
}
}typing:start
Another participant started typing. Show a typing indicator.
{
"type": "typing:start",
"payload": {
"conversationId": "conv-uuid",
"userId": "user-uuid",
"userName": "Support Agent",
"senderRole": "operator"
}
}The server does not broadcast this back to the sender — only to other participants in the room.
typing:stop
Another participant stopped typing. Remove the typing indicator.
{
"type": "typing:stop",
"payload": {
"conversationId": "conv-uuid",
"userId": "user-uuid"
}
}conversation:accepted
An operator accepted a pending conversation. Sent to all staff connections in the organization.
{
"type": "conversation:accepted",
"payload": {
"conversationId": "conv-uuid",
"assignedTo": "operator-uuid",
"assignedToName": "Support Agent"
}
}conversation:released
An operator left a conversation, returning it to pending status.
{
"type": "conversation:released",
"payload": {
"conversationId": "conv-uuid"
}
}conversation:resolved
A conversation was marked as closed.
{
"type": "conversation:resolved",
"payload": {
"conversationId": "conv-uuid",
"resolvedBy": "operator-uuid"
}
}messages:sync
Missed messages delivered on reconnection (when lastMessageId was provided in room:join).
{
"type": "messages:sync",
"payload": {
"conversationId": "conv-uuid",
"messages": [
{
"id": "msg-uuid-1",
"senderId": "operator-uuid",
"senderName": "Support Agent",
"senderRole": "operator",
"content": "Are you still there?",
"contentFormat": "plain",
"contentHtml": null,
"type": "text",
"createdAt": "2025-01-15T10:35:00.000Z"
}
]
}
}conversation:new
A new conversation was created. Sent to all staff connections in the organization.
{
"type": "conversation:new",
"payload": {
"id": "conv-uuid",
"organizationId": "org-uuid",
"applicationId": "app-uuid",
"status": "pending",
"subject": "Help with my order",
"createdAt": "2025-01-15T10:30:00.000Z"
}
}error
An error occurred processing your event.
{
"type": "error",
"payload": {
"code": "VALIDATION_ERROR",
"message": "Content is required"
}
}Error codes:
| Code | When |
|---|---|
PARSE_ERROR | Message could not be parsed as JSON |
VALIDATION_ERROR | Message structure or fields are invalid |
FORBIDDEN | Not a participant or not authorized for the action |
UNAUTHORIZED | Authentication failed during connection |
INVALID_TOKEN | WebSocket token is malformed or tampered |
EXPIRED_TOKEN | WebSocket token has expired |
ORIGIN_MISMATCH | Token origin does not match the connection origin |
APP_NOT_FOUND | Application from the token was not found |
RATE_LIMITED | Too many messages sent in a short period |
MESSAGE_NOT_FOUND | Target message for edit/delete does not exist |
EDIT_WINDOW_EXPIRED | The 15-minute edit/delete window has passed |
This list is exhaustive for the current protocol version.
pong
Heartbeat response.
{
"type": "pong"
}Reconnection
Clients should implement automatic reconnection with missed-message recovery:
Reconnection flow
Store the last message ID
After receiving each message:new or message:ack, save the message ID to local storage.
Detect disconnection
Listen for the WebSocket close event. Start a reconnection timer with exponential backoff (e.g. 1s, 2s, 4s, 8s, max 30s).
Reconnect and rejoin rooms
Open a new WebSocket connection. For each conversation the user was viewing, send room:join with the stored lastMessageId.
Process missed messages
The server responds with messages:sync containing all messages sent after your lastMessageId. Merge them into your local state.
Reconnection Example
let lastMessageId = localStorage.getItem("lastMessageId");
let reconnectDelay = 1000;
function connect() {
const ws = new WebSocket(`wss://api.deliverychat.online/v1/ws?token=${token}`);
ws.onopen = () => {
reconnectDelay = 1000;
// Rejoin rooms with last known message ID
ws.send(JSON.stringify({
type: "room:join",
payload: { conversationId, lastMessageId }
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "message:new") {
lastMessageId = data.payload.id;
localStorage.setItem("lastMessageId", lastMessageId);
}
if (data.type === "messages:sync") {
const messages = data.payload.messages;
if (messages.length > 0) {
lastMessageId = messages[messages.length - 1].id;
localStorage.setItem("lastMessageId", lastMessageId);
}
}
};
ws.onclose = () => {
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
};
}
connect();Heartbeat
Send ping events periodically (every 30 seconds recommended) to keep the connection alive. The server responds with pong.
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "ping" }));
}
}, 30000);