RealtimeUsing the WebSocket

Realtime

Using the WebSocket

One socket per browser tab or bot process. Agents, bots and visitors use the same endpoint and envelope; the ticket decides who you are.

Connect

  1. Get a ticket: agents and bots POST /v1/ws_ticket (bot token); visitors POST /v1/customer/ws_ticket (customer token). The response holds url.
  2. Open url: wss://<same host>/v1/ws?v=1&ticket=<ticket>.
  3. Send login within 10 s (see Client → server actions). Nothing else is accepted before it.

Ticket. 32 random bytes, base64url. It binds the principal kind, organization, agent or customer ID, and for visitors the verified host_origin.

Upgrade checks (failures are plain HTTP, no socket):

Check Agent session Bot Customer
Ticket valid and unused 401 401 401
Extra session still valid token not revoked not banned (403); host_origin in the ticket still in allowed_domains (403); org under its concurrent-visitor cap (429)

The host page's origin is checked against the organization's allowed_domains when the token and the ticket are issued (host_origin), and the iframe itself is served with Content-Security-Policy: frame-ancestors <allowed_domains>, which the browser enforces and a page can't spoof.

Envelope

JSON text frames. Max inbound frame 16 KiB.

ExampleJSON
// client → server request
{ "request_id": "c-42", "action": "send_event", "payload": { … } }
// server → client response (exactly one per request_id)
{ "request_id": "c-42", "action": "send_event", "type": "response", "success": true,  "payload": { … } }
{ "request_id": "c-42", "action": "send_event", "type": "response", "success": false, "payload": { "error": { "code": "conflict", "message": "chat is closed" } } }
// server → client push
{ "action": "incoming_event", "type": "push", "payload": { … } }

While the account is read-only, send_event from agents and visitors fails with subscription_locked, the visitor login answers online: false and the widget shows its offline form; typing, receipts, page updates and set_routing_status still work.

request_id is any client string up to 64 chars. Error codes are the API’s (validation, forbidden, not_found, conflict, rate_limited, …). An unknown action gets success:false with validation; an unparseable frame closes with 4002.

Resume and gap detection

  • The client keeps last_seq per chat it has open (the highest contiguous seq applied).
  • On every (re)connect it sends login {resume: {chat_id: last_seq, …}}.
  • login response resume: {<chat_id>: {replayed_to: seq, truncated: bool}}. truncated (or a chat the client didn't list) means fetch the chat again with get_chat or list_threads (both APIs have them).
  • State pushes (threads, tags, receipts, routing, traffic) are not replayed. After any reconnect call list_chats again (visitors: get_chat).
  • Gap rule for incoming_event: seq ≤ last_seq → duplicate, drop. seq = last_seq + 1 → apply. seq > last_seq + 1 → hold it, fetch the missed events with the API (list_threads), apply in order. A state push whose seq is ahead of last_seq is also a gap signal.
  • Seq is per chat across its threads, so a resumed chat (new thread) continues the same sequence.

Idempotency

client_message_id (UUID, v7 preferred) is required on every send_event, on the socket or with the API. A retry with the same ID returns the original event and duplicate: true, whatever the retried body says. Retry the same ID after any timeout or reconnect; never mint a new one for the same user action.

Heartbeats and presence

  • Server sends a WebSocket ping every 25 s; no pong within 10 s → the server drops the connection (the client sees an abnormal close, 1006, and reconnects).
  • Client sends ping every 25 s (browsers can't see protocol pings). No frame of any kind within 10 s after its ping → the client closes and reconnects.
  • An agent with no live socket for 30 s becomes offline: their chats move to accepting agents in the group, else the queue. Signing out applies this at once.
  • Visitor presence drives Traffic: first socket → customer_visit_started; no socket for 60 s → customer_visit_ended.

Close codes and client reaction

Code Reason Client does
1000 normal (sign-out, chat-only socket closed by client) nothing
1009 frame over 16 KiB fix the client; reconnect
1011 server error reconnect with backoff
1012 service restart reconnect after a random 0 to 5 s, then resume
4000 no login within 10 s reconnect with backoff
4001 session or token revoked/expired bots: stop, the token is no longer valid; visitors: POST /v1/customer/token again, then a new ticket
4002 protocol error (unparseable frame) fix the client; reconnect with backoff
4003 forbidden (customer banned, agent removed) do not reconnect; visitors show the ended state
4008 rate limit exceeded (see Rate limits) reconnect after 30 s
4009 slow consumer: send buffer (64 frames) full reconnect at once and resume; nothing is lost

Backoff: full jitter, 0.5 s doubling to 30 s, reset after a socket has stayed up 60 s. Every reconnect needs a new ticket.

Backpressure. Each socket has a bounded send buffer of 64 frames. Pushes about the same chat are never dropped silently.

Rate limits

Per visitor socket (public, untrusted):

What Limit Over the limit
send_event 20 per 10 s, burst 5 success:false, rate_limited
send_sneak_peek 2 per s extra frames dropped silently (latest wins)
send_typing_indicator 2 per s dropped silently
mark_events_as_* coalesced to 1 write per s: a mark inside the second is acknowledged with watermark: null and the latest per chat is written when the second ends
update_customer_page 30 per min rate_limited
all frames 50 per 10 s close 4008

Visitor connections: 5 sockets per customer, 20 per IP, and a per-organization concurrent cap (default 5000) enforced at POST /v1/customer/ws_ticket (429). Ticket issue: 30/min per customer.

Agents and bots: send_event 30 per 10 s, all frames 100 per 10 s (close 4008), 10 sockets per agent.

Message text is 1 to 4000 characters everywhere.