Skip to Content
🎉 Welcome to Delivery Chat Documentation
V1Rest APIWebSocket

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-token

cURL 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" } }
FieldTypeRequiredDescription
conversationIdstring (UUID)YesConversation to join
lastMessageIdstring (UUID)NoLast 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" } }
FieldTypeRequiredDescription
conversationIdstring (UUID)YesTarget conversation
contentstringYesMessage text (max 10,000 chars). For rich text, this is a Lexical JSON string.
contentFormatstringNo"plain" (default) or "lexical". See Message Formats.
clientMessageIdstringYesClient-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:

CodeWhen
PARSE_ERRORMessage could not be parsed as JSON
VALIDATION_ERRORMessage structure or fields are invalid
FORBIDDENNot a participant or not authorized for the action
UNAUTHORIZEDAuthentication failed during connection
INVALID_TOKENWebSocket token is malformed or tampered
EXPIRED_TOKENWebSocket token has expired
ORIGIN_MISMATCHToken origin does not match the connection origin
APP_NOT_FOUNDApplication from the token was not found
RATE_LIMITEDToo many messages sent in a short period
MESSAGE_NOT_FOUNDTarget message for edit/delete does not exist
EDIT_WINDOW_EXPIREDThe 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

1

Store the last message ID

After receiving each message:new or message:ack, save the message ID to local storage.

2

Detect disconnection

Listen for the WebSocket close event. Start a reconnection timer with exponential backoff (e.g. 1s, 2s, 4s, 8s, max 30s).

3

Reconnect and rejoin rooms

Open a new WebSocket connection. For each conversation the user was viewing, send room:join with the stored lastMessageId.

4

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);
Last updated on