Transport: WebSocket, HTTP fallback and reconnecting
How the widget talks to the chat server – addresses, subprotocol rc.v1, frame format, error and close codes, limits, reconnecting with seq and deduplication, and the HTTP fallback via Server-Sent Events and long polling.
This page describes what happens between the widget in the browser and the chat server. You need it for troubleshooting (network tab of the developer tools), for proxy and firewall rules and for your Content Security Policy.
Important: the wire protocol is the interface between our widget and our server. Custom clients that speak the protocol directly are not officially supported; fields may be extended within protocol version 1. For your own interfaces use the JavaScript API, for server connections webhooks and the server API.
Addresses
- WebSocket:
wss://ws.reactive.chat/rc_wss/ - HTTP fallback, offline form, uploads: the same address with
https://, i.e.https://ws.reactive.chat/rc_wss/h/…,https://ws.reactive.chat/api/offline/<SITE_KEY>andhttps://ws.reactive.chat/rc_wss/upload/… - Widget files:
https://cdn.reactive.chat/
How the widget finds the WebSocket address:
data-rc-ws-urlon the loader tag,window.ReactiveChatSettings.wsUrlorReactiveChat('boot', { wsUrl })– if set, this value applies.- Otherwise it is derived from the location of
loader.js:wss://ws.+ the last two labels of the domain +/rc_wss/.cdn.reactive.chatbecomeswss://ws.reactive.chat/rc_wss/.
Two pitfalls follow from this:
- If you serve
loader.jsfrom your own domain, the widget derives an address under your domain. In that case, setdata-rc-ws-url(see Quick start). - With two-part endings such as
.co.uk, the derivation goes wrong. Setdata-rc-ws-urlexplicitly.
The WebSocket route does not evaluate query parameters.
Establishing the connection
- Timing: the widget does not connect while the page loads but on the first real interaction (mouse movement, click, key press, mouse wheel, touch, focus). It connects immediately if the chat window is open or being opened, a conversation of the visitor is known, or unsent messages are waiting. With
data-rc-connect="load"on any element (usually the loader tag) orwindow.ReactiveChatSettings.connect = 'load'it connects on load as before. Rules that fire a certain time after the page view count from the first interaction. - Subprotocol: the widget opens
new WebSocket(url, 'rc.v1'), and the server mirrorsrc.v1. The protocol version is not in the address but in every frame (v: 1). - Text frames only, in UTF-8; one frame = one message. No compression (
permessage-deflateis off on the server side). - Handshake: after
open, the widget sendsinitwithin 5 seconds at the latest; the server responds withinit_ok. Ifopendoes not come within 8 seconds, the attempt counts as failed; ifinit_okdoes not come within 10 seconds, the widget reconnects. - Origin check: the server compares the host name of the
Originheader with the project's domains (Allowed origins in your account). Scheme and port do not count; subdomains match (www.your-company.commatchesyour-company.com,notyour-company.comdoes not). On failure: errororigin_denied, then close 1008, no retry. If no domain is entered in the project, no origin check takes place – so always enter your domains.
Frame format
Every message in both directions has this form:
{ "v": 1, "t": "msg", "id": "01JAB3C4D5E6F7G8H9JKMNPQRS", "ts": 1792500000000, "d": { } }
- v (integer, required) – protocol version, currently
1. - t (string, required) – message type,
[a-z0-9_]{1,24}. - id (string, required) – ULID: 26 characters of Crockford Base32, uppercase only. It is also the key for deduplication.
- ts (integer, required) – milliseconds since 1970, for diagnostics only.
- d (object, required) – payload, never
null,{}when empty.
Unknown types and unknown fields are silently ignored – this keeps older widgets compatible with newer servers.
Message types at a glance
From the widget to the server:
- init – registration of the connection: site key, session, visitor, tab and conversation ID, language, consent state, the signed identity if any (
user), page context (ctx) and resume point (resume). - msg – a visitor message (text, up to 5 attachments, reply to quick replies or forms).
- typing – typing indicator (
start/stop). The content of the input field is never transmitted. - read – read up to
seq. - ctx – context change, e.g. on navigation in an SPA or after
identify. - csat – rating (thumbs up/down, comment up to 1000 characters).
- upload_req – request for upload approval (name, MIME type, size, SHA-256).
- transcript, forget, consent – conversation by email, delete own data, grant/withdraw consent.
- ping – keep-alive.
From the server to the widget:
- init_ok – confirmed IDs, history, design, features and limits (see below), result of the identity check (
identity.verified). - msg – messages (including the echo of the visitor's own message with
client_id). - bot_typing, bot_stream – the AI assistant is thinking or writing;
bot_streamalways transmits the whole text visible so far, at most every 150 ms. - typing, read, handoff, queue_pos, proactive, conv_list, error, pong.
Error codes
An error arrives as a frame t: "error" with d.code, d.message (English, for developers only), d.retry (no, immediate, backoff, after_reinit) and, where needed, d.retry_after_ms and d.detail. The list is closed; an unknown code is treated like internal.
- bad_json – not valid JSON; after three occurrences close 1008.
- bad_envelope – the frame violates the format (also: ULID more than 24 h off,
detail.reason: stale_or_future). - unsupported_version – version cannot be negotiated; close 4001.
- unknown_site_key – site key unknown, project paused or archived; close 4002.
- origin_denied – origin not among the project's domains; close 1008.
- visitor_blocked – visitor blocked; close 1008.
- auth_failed – identity check failed,
detail.reason:no_hash,bad_hash,bad_exp,expired,exp_too_far. The session continues anonymously. - not_initialized / already_initialized – message before
init, or a secondinit. - init_timeout – no
initwithin 5 s; close 4004. - conversation_not_found / conversation_closed
- rate_limited – rate limit; with
retry_after_ms. If it happens repeatedly on the same connection, the server closes with 4008. - payload_too_large – frame over 64 KiB.
- msg_too_long – text longer than the plan limit,
detail.max. - unsupported_kind, attachment_invalid
- upload_disabled, upload_too_large, upload_mime_denied, upload_expired
- consent_required – the action requires consent.
- dnd_active – “Do not disturb” is active.
- session_conflict – too many connections of the same session (more than 3); close 4003, the tab becomes a follower.
- internal, unavailable – temporary; retry with a delay.
Close codes
- 1000, 1001, 1006, 1011, 1013, 4004 – the widget reconnects (with a delay).
- 1008 – origin rejected, repeatedly invalid JSON, block by the abuse protection. No retry on this page.
- 4001 – version cannot be negotiated. No retry.
- 4002 – site key unknown or project inactive. No retry.
- 4008 – repeated rate limiting. The widget waits for
retry_after_msand reconnects. - 4003 – session conflict; the tab runs as a follower of another tab.
Limits per connection
The server sends the binding values in init_ok.limits and init_ok.features:
- heartbeat_ms 25 000 – the widget sends a
ping25 s after the last outgoing message. - pong_timeout_ms 10 000 – two missing
pongs lead to a reconnect. - idle_timeout_ms 70 000 – the server closes after 70 s without an incoming frame (1001).
- max_frame_bytes 65 536 – larger frames:
payload_too_large. - msg_per_10s 20 and frames_per_10s 120 – token bucket per connection; above that
rate_limited. - reconnect_cap_ms 30 000 – longest delay between retries.
- dedup_window_ms 86 400 000 – 24 hours of deduplication.
- features.max_msg_len – characters (Unicode code points) per visitor message: 1000 on the Free plan, 4000 on paid plans. The widget does not send texts that are too long.
- features.max_upload_bytes, features.allowed_mime – see Limits and quotas.
Reconnecting, order, duplicates
- Delay:
min(30000, 500 × 2^(attempt−1))ms, of which a random value in the upper half is used (attempt 1: 250–500 ms, from attempt 7: 15–30 s). Three times as long in background tabs. Aretry_after_msfrom the server is the lower bound. The number of attempts is unlimited; the counter only resets oninit_ok. If the browser reportsonline, the widget tries immediately; if it is offline, it waits. - Order: every message of a conversation carries a gapless
seqstarting at 1. Messages are displayed byseq, not by time. On reconnect, the widget sendsresume.last_seqand up to 50 pending IDs; the server delivers everything after that (at most 100 messages, otherwisehas_more) and repeats the confirmations for messages that have already arrived. - No duplicates: the server remembers every message
idfor 24 hours. A repeated message is not stored again but gets the same confirmation. “Send again” in the widget uses the sameid. - States of an outgoing message:
pending→sent→delivered(echo from the server) →read;failedafter 5 attempts, after 24 h or onretry: "no". - Offline buffer: if the connection drops, messages go into a buffer (at most 50 entries, 128 KiB, 24 h) – with consent in
localStorage, without it insessionStorage– and go out on the nextinit_ok. - Several tabs, one connection: the tabs of a website elect a leader (Web Locks, otherwise a keep-alive in
localStorage); only the leader holds the connection, and the others communicate viaBroadcastChannel.
HTTP fallback
If a network does not let a WebSocket through, the chat continues over ordinary HTTPS requests – with the same frames and the same limits. Only the widget uses it; the agent interface always connects via WebSocket. The short version for editors is under HTTP fallback.
When it applies:
- the browser does not support WebSocket;
- two WebSocket attempts fail in a row before a WebSocket has ever been established on this page (an attempt without a result after 8 seconds counts as failed);
sessionStoragecontains an entryrc_httpthat is less than 30 minutes old (following pages in the same tab);- you force it with
data-rc-transport="http"(on any element that is in the document at start-up) or withwindow.ReactiveChatSettings = { transport: 'http' }before the loader.ReactiveChat('boot', { transport: 'http' })has no effect – the connection type is fixed at start-up.
Without fetch in the browser there is no fallback. If an existing WebSocket connection drops, the widget reconnects via WebSocket. After a fallback, it quietly checks once per page whether a WebSocket now gets through; if it does, it deletes rc_http, and the next page uses the WebSocket again.
The requests (under https://ws.reactive.chat/rc_wss/h/):
- POST h/auf – opens the connection. Response
{"v":1,"cid":"…","tok":"…","poll_ms":25000,"idle_ms":60000}.tokis a short-lived connection token (32 characters). - GET h/<cid>/sse?tok=… – Server-Sent Events,
id:anddata:per frame, a keep-alive every 15 s. Missed frames are delivered viaLast-Event-ID. - GET h/<cid>/poll?tok=…&nach=<n> – long polling; the server holds the request open for up to 25 s and responds with
{"v":1,"n":…,"rahmen":[…]}. - POST h/<cid>/tx?tok=… – send,
text/plain, one frame or an array of at most 20 frames; response 204. - POST h/<cid>/zu?tok=… – close (via
sendBeacon).
The widget tries SSE first; if no first frame arrives within 5 seconds (for example because a proxy buffers), it switches to long polling. It treats errors such as 404 (unbekannt), 403, 413 or 429 like a dropped connection and retries with a delay. After 60 seconds without a request, the server closes the connection. CORS: Access-Control-Allow-Origin mirrors the origin, without credentials; the origin check takes place in init, as with the WebSocket.
Requirements on your side:
connect-srcof your CSP contains the chat address withwss://andhttps://.- Firewalls and proxies allow requests lasting at least 30 seconds.
- Your own proxy passes
/rc_wss/through unbuffered (nginxproxy_buffering offandproxy_cache off, Apacheflushpackets=on, Caddyflush_interval -1) and does not write/rc_wss/h/to the access log (token in the query). See Own domain.
Stored in the browser: only the entry rc_http in sessionStorage with the time of the HTTPS connection, without personal data.
Which route is active
In the network tab of the developer tools:
- WebSocket: a connection of type WebSocket to
wss://ws.reactive.chat/rc_wss/(or your subdomain). In the frames you seeinit,init_ok,ping/pongand the messages. - HTTP fallback: first
h/auf, then a long-running…/sse(or recurring…/poll) and one…/txfor every message sent.
Next: Limits and quotas · Troubleshooting
อัปเดตล่าสุด: