Skip to content

WebSockets

CloudWA uses a persistent WebSocket connection to push real-time updates to connected agents and admins. Instead of polling the REST API, your client opens a single connection and reacts to server-sent events as they happen.

Connect to the WebSocket endpoint over the standard HTTP Upgrade mechanism:

wss://<your-domain>/api/ws
ParameterValue
ProtocolWebSocket (RFC 6455)
Path/api/ws
EncodingJSON (UTF-8)
Max message size512 bytes (client → server)
const ws = new WebSocket('wss://app.example.com/api/ws');
ws.addEventListener('open', () => {
// Step 1 — authenticate immediately
ws.send(JSON.stringify({
type: 'auth',
payload: { token: '<JWT_OR_API_KEY>' }
}));
});
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
console.log(msg.type, msg.payload);
});

Every client must send an auth message within 5 seconds of opening the connection. If the token is invalid or the deadline is missed, the server closes the connection with WebSocket close code 1008 (Policy Violation).

{
"type": "auth",
"payload": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
FieldTypeRequiredDescription
typestringMust be "auth"
payload.tokenstringJWT session token or an API Key

Once authenticated, the client is registered to receive events scoped to its organization.


The server sends a WebSocket Ping frame every 54 seconds (90% of the 60-second read deadline). The client must respond with a Pong frame to keep the connection alive. Standard WebSocket libraries handle this automatically.

DirectionFrame TypePeriod
Server → ClientPingevery 54 s
Client → ServerPongupon each Ping

Clients can tell the server which contact conversation they are currently viewing. This enables BroadcastToContact targeting — events such as conversation notes will only reach clients that have declared they are viewing that contact.

{
"type": "set_contact",
"payload": {
"contact_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}
FieldTypeRequiredDescription
typestringMust be "set_contact"
payload.contact_idstring (UUID)UUID of the contact whose conversation is open

To stop watching a contact (e.g., when navigating away), send set_contact with an empty string or omit contact_id.


All server-sent events share the same envelope:

{
"type": "<event_type>",
"payload": { ... }
}

Broadcast to the entire organization when a new WhatsApp message (incoming or outgoing) is saved and broadcast is enabled.

Broadcast scope: Organization-wide

{
"type": "new_message",
"payload": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"assigned_user_id": "b2c3d4e5-0000-0000-0000-000000000002",
"profile_name": "Ahmed Hassan",
"direction": "incoming",
"message_type": "text",
"content": { "body": "Hello, I need help with my order." },
"media_url": "",
"media_mime_type": "",
"media_filename": "",
"interactive_data": null,
"status": "pending",
"wamid": "wamid.HBgNMjAxMTIzNDU2Nzg5FQIAERgSM...",
"created_at": "2024-06-09T12:00:00Z",
"updated_at": "2024-06-09T12:00:00Z",
"is_reply": false
}
}

For reply messages, additional fields are included:

{
"type": "new_message",
"payload": {
"...(all base fields above)...",
"is_reply": true,
"reply_to_message_id": "449e7300-e29b-41d4-a716-446655440099",
"reply_to_message": {
"id": "449e7300-e29b-41d4-a716-446655440099",
"content": { "body": "Previous message text" },
"message_type": "text",
"direction": "outgoing"
}
}
}
FieldTypeDescription
idUUIDMessage ID
contact_idUUIDContact who sent/received the message
assigned_user_idUUID | nullAgent currently assigned to this contact
profile_namestringContact’s display name (may be masked)
direction"incoming" | "outgoing"Message direction
message_typestringtext, image, video, audio, document, interactive, template, flow
content.bodystringText content
media_urlstringURL for media messages
statusstringpending, sent, delivered, read, failed
wamidstringWhatsApp message ID from Meta
is_replybooleanWhether this is a reply to another message

Broadcast to the organization when a message delivery status changes (sent, delivered, read, or failed).

Broadcast scope: Organization-wide

{
"type": "status_update",
"payload": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"status": "delivered",
"wamid": "wamid.HBgNMjAxMTIzNDU2Nzg5FQIAERgSM..."
}
}

On failure, the payload includes an error message:

{
"type": "status_update",
"payload": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"status": "failed",
"error_message": "Message failed to send: network timeout"
}
}
FieldTypeDescription
message_idUUIDMessage whose status changed
contact_idUUIDOwning contact
statusstringsent, delivered, read, or failed
wamidstringWhatsApp message ID (on success)
error_messagestringError detail (on failure only)

Broadcast when a contact reacts or un-reacts to a message.

Broadcast scope: Organization-wide

{
"type": "reaction_update",
"payload": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"reactions": [
{ "emoji": "👍", "user": "+201234567890" }
]
}
}

Broadcast when a new agent transfer (human handoff) is created. All agents in the organization receive this to update their queue view.

Broadcast scope: Organization-wide

{
"type": "agent_transfer",
"payload": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"contact_name": "Ahmed Hassan",
"phone_number": "+201234567890",
"whatsapp_account": "support",
"status": "active",
"source": "chatbot",
"notes": "Customer needs billing support",
"transferred_at": "2024-06-09T12:05:00Z",
"agent_id": "b2c3d4e5-0000-0000-0000-000000000002",
"team_id": "c3d4e5f6-0000-0000-0000-000000000003"
}
}
FieldTypeDescription
idUUIDTransfer ID
contact_idUUIDContact being transferred
contact_namestringContact display name (may be masked)
phone_numberstringContact phone (may be masked)
whatsapp_accountstringWhatsApp account name
statusstringactive, completed, resumed
sourcestringchatbot, keyword, api, manual
notesstringOptional transfer notes
transferred_atISO 8601When the transfer was created
agent_idUUID | nullAssigned agent (if any)
team_idUUID | nullAssigned team (if any)

Broadcast when an agent resumes a transfer (handing control back to the chatbot).

Broadcast scope: Organization-wide

{
"type": "agent_transfer_resume",
"payload": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"status": "resumed",
"resumed_at": "2024-06-09T13:10:00Z",
"resumed_by": "b2c3d4e5-0000-0000-0000-000000000002"
}
}

Broadcast when a transfer is assigned to (or unassigned from) an agent.

Broadcast scope: Organization-wide

{
"type": "agent_transfer_assign",
"payload": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"status": "active",
"agent_id": "b2c3d4e5-0000-0000-0000-000000000002",
"team_id": null
}
}

When agent_id is null, the transfer was returned to the queue.


Sent when an SLA deadline is approaching and the transfer needs attention.

Broadcast scope: Organization-wide

{
"type": "transfer_escalation",
"payload": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"contact_name": "Ahmed Hassan",
"phone_number": "+201234567890",
"status": "active",
"escalation_level": 1,
"sla_breached": false
}
}

Sent when a transfer has exceeded its SLA resolution deadline.

Broadcast scope: Organization-wide

{
"type": "transfer_expired",
"payload": {
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"contact_name": "Ahmed Hassan",
"phone_number": "+201234567890",
"status": "active",
"escalation_level": 2,
"sla_breached": true
}
}

Sent after the transfer has been escalated to a higher level.

Broadcast scope: Organization-wide

Same payload shape as transfer_expired.


These events are targeted — only clients that have called set_contact for the relevant contact receive them.

Broadcast scope: Clients viewing the contact

{
"type": "conversation_note_created",
"payload": {
"id": "d4e5f6a7-0000-0000-0000-000000000001",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"content": "Customer confirmed order number is #5523",
"created_by_id": "b2c3d4e5-0000-0000-0000-000000000002",
"created_by_name": "Sara Ahmed",
"created_at": "2024-06-09T12:15:00Z",
"updated_at": "2024-06-09T12:15:00Z"
}
}

Broadcast scope: Clients viewing the contact

Same payload shape as conversation_note_created.


Broadcast scope: Clients viewing the contact

{
"type": "conversation_note_deleted",
"payload": {
"id": "d4e5f6a7-0000-0000-0000-000000000001",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001"
}
}

Broadcast in real-time as campaign messages are sent and their statuses are updated.

Broadcast scope: Organization-wide

{
"type": "campaign_stats_update",
"payload": {
"campaign_id": "e5f6a7b8-0000-0000-0000-000000000001",
"status": "running",
"sent_count": 1042,
"delivered_count": 980,
"read_count": 650,
"failed_count": 12
}
}
FieldTypeDescription
campaign_idUUIDCampaign being updated
statusstringdraft, scheduled, running, completed, cancelled
sent_countintegerMessages sent so far
delivered_countintegerConfirmed deliveries
read_countintegerConfirmed reads
failed_countintegerFailed sends

Sent to a specific user when their role or permissions are modified.

Broadcast scope: Targeted user only

{
"type": "permissions_updated",
"payload": {
"message": "Your permissions have been updated"
}
}

Upon receiving this event, the client should re-fetch its user profile or reload the application to apply the new permissions.


All calling events carry a payload object built from the active call session. The specific fields depend on the call state.

Sent to the assigned sticky agent (or broadcast org-wide if no sticky agent) when an inbound WhatsApp call arrives.

Broadcast scope: Specific agent user or organization-wide

{
"type": "call_incoming",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"whatsapp_call_id": "CALL_ABC123",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"contact_phone": "+201234567890",
"whatsapp_account": "support",
"direction": "incoming",
"started_at": "2024-06-09T14:00:00Z"
}
}

Sent org-wide when an agent answers a call.

Broadcast scope: Organization-wide

{
"type": "call_answered",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"whatsapp_call_id": "CALL_ABC123",
"agent_id": "b2c3d4e5-0000-0000-0000-000000000002",
"answered_at": "2024-06-09T14:00:05Z"
}
}

Sent when a call is terminated.

Broadcast scope: Organization-wide

{
"type": "call_ended",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"whatsapp_call_id": "CALL_ABC123",
"duration_seconds": 125,
"ended_at": "2024-06-09T14:02:05Z"
}
}

Sent when the active call is placed on hold or resumed.

Broadcast scope: Organization-wide

{
"type": "call_hold",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"held_at": "2024-06-09T14:01:00Z"
}
}

Sent to the initiating agent when an outbound WhatsApp call is placed.

Broadcast scope: Specific agent user

{
"type": "outgoing_call_initiated",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"contact_id": "a1b2c3d4-0000-0000-0000-000000000001",
"contact_phone": "+201234567890",
"whatsapp_account": "support",
"initiated_at": "2024-06-09T14:30:00Z"
}
}
{
"type": "outgoing_call_ringing",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001"
}
}
{
"type": "outgoing_call_answered",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"answered_at": "2024-06-09T14:30:08Z"
}
}
{
"type": "outgoing_call_rejected",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"reason": "busy"
}
}
{
"type": "outgoing_call_ended",
"payload": {
"id": "f6a7b8c9-0000-0000-0000-000000000001",
"duration_seconds": 74,
"ended_at": "2024-06-09T14:31:22Z"
}
}

Sent to the target agent when an active call is being transferred to them, requesting acceptance.

Broadcast scope: Specific agent user

{
"type": "call_transfer_waiting",
"payload": {
"id": "a9b0c1d2-0000-0000-0000-000000000001",
"call_log_id": "f6a7b8c9-0000-0000-0000-000000000001",
"whatsapp_call_id": "CALL_ABC123",
"from_agent_id": "b2c3d4e5-0000-0000-0000-000000000002",
"contact_phone": "+201234567890"
}
}

Sent when the target agent accepts the transfer.

Broadcast scope: Specific agent user

{
"type": "call_transfer_connected",
"payload": {
"id": "a9b0c1d2-0000-0000-0000-000000000001",
"connected_at": "2024-06-09T14:32:00Z"
}
}

Broadcast when the transfer finishes successfully.

Broadcast scope: Organization-wide

{
"type": "call_transfer_completed",
"payload": {
"id": "a9b0c1d2-0000-0000-0000-000000000001"
}
}

Broadcast when the caller hangs up before the transfer is completed.

Broadcast scope: Organization-wide

{
"type": "call_transfer_abandoned",
"payload": {
"id": "a9b0c1d2-0000-0000-0000-000000000001"
}
}

Sent to the target agent when the transfer times out with no answer.

Broadcast scope: Specific agent user

{
"type": "call_transfer_no_answer",
"payload": {
"id": "a9b0c1d2-0000-0000-0000-000000000001"
}
}

Sent to the previous agent when a rotating transfer has been reassigned to another agent.

Broadcast scope: Specific agent user

{
"type": "call_transfer_reassigned",
"payload": {
"id": "a9b0c1d2-0000-0000-0000-000000000001",
"reason": "timeout"
}
}
reason valueDescription
"timeout"Per-agent ring timeout expired
"total_timeout"Total transfer timeout expired

Sent org-wide when calling permissions change (e.g., a team’s calling access is toggled).

Broadcast scope: Organization-wide

{
"type": "call_permission_update",
"payload": {
"message": "Calling permissions updated"
}
}

Event TypeBroadcast ScopeTrigger
new_messageOrganizationNew incoming or outgoing message
status_updateOrganizationMessage delivery status change
reaction_updateOrganizationMessage reaction added/removed
agent_transferOrganizationNew human handoff transfer created
agent_transfer_resumeOrganizationTransfer resumed (chatbot takes over)
agent_transfer_assignOrganizationTransfer assigned/unassigned to agent
transfer_escalationOrganizationSLA escalation warning
transfer_expiredOrganizationSLA deadline breached
transfer_escalatedOrganizationTransfer escalated to higher level
conversation_note_createdContact viewersNew note on a conversation
conversation_note_updatedContact viewersNote edited
conversation_note_deletedContact viewersNote removed
campaign_stats_updateOrganizationCampaign progress update
permissions_updatedSpecific userUser’s permissions changed
call_incomingAgent or OrgInbound call arriving
call_answeredOrganizationCall answered by agent
call_endedOrganizationCall ended
call_holdOrganizationCall placed on hold
call_resumedOrganizationCall taken off hold
outgoing_call_initiatedAgentOutbound call placed
outgoing_call_ringingAgentRemote phone is ringing
outgoing_call_answeredAgentRemote answered
outgoing_call_rejectedAgentRemote rejected the call
outgoing_call_endedAgentOutbound call ended
call_transfer_waitingTarget agentIncoming transfer request
call_transfer_connectedTarget agentTransfer accepted
call_transfer_completedOrganizationTransfer completed successfully
call_transfer_abandonedOrganizationCaller hung up during transfer
call_transfer_no_answerTarget agentTransfer timed out
call_transfer_reassignedPrevious agentTransfer moved to next agent
call_permission_updateOrganizationCalling permissions changed

The WebSocket connection can be closed by the server in the following scenarios:

Close CodeReason
1008Authentication failed or auth timeout
1001Server shutting down
1006Abnormal closure (network error)

Recommended reconnection strategy:

function connect() {
const ws = new WebSocket('wss://app.example.com/api/ws');
let retryDelay = 1000;
ws.addEventListener('close', (event) => {
if (event.code === 1008) {
console.error('Auth failed — do not retry without a new token');
return;
}
// Exponential backoff
setTimeout(connect, retryDelay);
retryDelay = Math.min(retryDelay * 2, 30000);
});
ws.addEventListener('open', () => {
retryDelay = 1000; // Reset on successful connection
ws.send(JSON.stringify({ type: 'auth', payload: { token: getToken() } }));
});
}
connect();