Realtime overview
This page describes the three WebSockets Open Play offers and when to use each. For in-game state (movement, room, scene) you use the plugin realtime socket, and for notifications and chat you use two separate sockets; all of them have different identity and close rules.
Three sockets
| Socket | Path | What for | Who speaks | Credential |
|---|---|---|---|---|
| Realtime (plugin) | /v1/realtime/ws | Game state: live input, state snapshot, durable commands and events | Two-way | Single-use ticket |
| Notification | /v1/ws | Pushing a notification to the player live | Server → player only | Player session (cookie or Bearer) |
| Chat | /v1/chat/ws | Channel chat (subscribe, send a message) | Two-way | Player session (cookie or Bearer) |
Rule: all three are player sockets. A socket is not opened with a server key; for server-to-server work use the REST endpoints.
Which one should I use
| Need | Choice |
|---|---|
| Publishing the live state of a vehicle, a player, or a room and receiving player input in order | The realtime socket, protocol |
| Getting the result of a command durably and replay-safely (reward, delivery) | The realtime Command frame or the HTTP command endpoint (calling plugins) |
| Showing the player a “new notification” instantly | /v1/ws; the durable source is always the GET /v1/notifications inbox (notifications) |
| Channel chat | /v1/chat/ws or REST (chat) |
| Leaderboard, store, inventory | REST; there is no socket for these |
Note
The TypeScript SDK manages the realtime socket for you: getting the ticket, authentication, reconnecting, and snapshot recovery are included. You need to write the protocol by hand only for a client without an SDK (for example the Unity native socket). For SDK usage, see the TypeScript SDK page.
Connection requirements (shared)
- HTTPS and an allowed origin. Your browser game calls the API from the same origin through a reverse proxy or, if its domain is registered in an environment, directly at the Open Play address (CORS). Sockets open to the same address:
wss://<your-game-domain>/<path>orwss://<open-play-address>/<path>; the handshake checksOriginper environment (CORS does not change this). Details: architecture and hosting. - Environment header. A player identity is always bound to an environment and requires the
x-crx-env: <environment uuid>header. On the realtime socket the header is given with the HTTP ticket request; the WebSocket upgrade carries no header, the ticket knows the environment./v1/wsand/v1/chat/ws, however, require the header on the upgrade request itself. The browser’s WebSocket API cannot add custom headers; therefore these two sockets cannot be opened directly from the browser (a known limitation). In browser games, poll the inbox with REST for notifications and use the REST endpoints for chat; non-browser clients can send the header themselves. - No token in the query string. No socket accepts a session token in the URL;
?token=is not supported so that it does not leak into access logs. A realtime upgrade that carries a query string is rejected. - The Origin header is required (realtime): both the ticket request and the upgrade must carry
Origin, and it must be one of the environment’s allowed origins. The browser adds it itself; if it is a non-browser client, you must send an allowedOriginvalue yourself. - Module gate. The realtime socket depends on the
pluginsmodule,/v1/wsonnotifications, and/v1/chat/wsonchat. If the module is disabled, the upgrade returns404andmodule_disabled; an open socket is closed with1008(reasonmodule_disabled) if the module is disabled later, and with1013if the gate state cannot be verified. You manage modules from the panel: Game settings: modules.
Notification socket (/v1/ws)
- It flows only from the server to the player. Frames coming from the client are ignored.
- The incoming frame is a JSON object:
{"type":"notification","notification_id":"…","title":"…","body":"…","data":{…},"channel":"in_game","sent_at_ms":1760000000000}. Field names aresnake_case. - A live frame is delivered at most once. The durable source is the inbox; the same notification can arrive both as a frame and through an inbox query, so deduplicate by
notification_id. - The connection counts as a presence (online) signal.
Chat socket (/v1/chat/ws)
Frames are distinguished by the type field (with camelCase values); the other field names are snake_case, and an unknown field is rejected. Example: {"type":"send","channel_id":"…","client_message_id":"retry-1","body":"hello"}.
| Direction | type | Fields | Meaning |
|---|---|---|---|
| Client → server | subscribe | channel_id | Channel subscription (one channel at a time) |
| Client → server | unsubscribe | — | Drops the subscription |
| Client → server | send | channel_id, client_message_id, body | Replay-safe message sending, the same as REST |
| Client → server | ping | nonce (number) | Liveness probe |
| Server → client | subscribed / unsubscribed | channel_id (only subscribed) | Accepted |
| Server → client | message | message, duplicate | Channel message; if duplicate: true, the client_message_id had been processed before |
| Server → client | pong | nonce | The reply to ping |
| Server → client | error | code | invalid_frame, invalid_payload, forbidden_channel, unavailable, message_rejected, module_disabled when the module is being disabled |
Message sending limit: 20 messages per 10 seconds per player; you see an overage as message_rejected or, in REST, as 429 (rate limits). For detailed usage, see the chat guide.
Next step
- To see the plugin realtime protocol from start to finish, see the protocol.
- To see what happens when the connection drops, the limits, and idle close, see reconnect and limits.