Wire protocol
Every frame on the wire — client actions, server responses, and delivery semantics.
All frames are JSON text messages. Client frames carry an action;
server frames carry a type. Every client frame accepts an optional
id — an opaque string the server echoes back in its response, so you
can correlate requests with replies.
Client → server
Every client frame is a JSON object with these fields:
| Field | Meaning |
|---|---|
action required | subscribe, unsubscribe, or ping. |
channel required | For subscribe / unsubscribe: the channel name — see Channels. Not used by ping. |
id optional | An opaque string the server echoes back in its response, so you can correlate requests with replies. |
subscribe
Start receiving events on a channel. Answered with an ack on success
or an error frame on rejection.
{ "action": "subscribe", "channel": "workspace:WORKSPACE_ID", "id": "c1" }unsubscribe
Stop receiving events on a channel. Always acknowledged, even if you weren't subscribed.
{ "action": "unsubscribe", "channel": "workspace:WORKSPACE_ID", "id": "c2" }ping
Application-level keepalive. Answered with a pong.
{ "action": "ping", "id": "c3" }Server → client
welcome
Sent once, immediately after a successful connect. Confirms who you are and which connection you hold.
{
"type": "welcome",
"connection_id": "9a1b2c3d4e5f60718293a4b5c6d7e8f9",
"user_id": "USER_ID",
"cell": "eu-1",
"ts": "2026-07-15T12:00:00+00:00"
}The cell field identifies the region serving your connection — it
matches the region of your base URL.
ack
Confirms a subscribe or unsubscribe. Echoes the action, channel,
and your id (null if you didn't send one).
{ "type": "ack", "action": "subscribe", "channel": "workspace:WORKSPACE_ID", "id": "c1" }event
A platform event on a channel you're subscribed to. event is the event
name (see the event catalog), data is the
event payload, ts is the ISO 8601 publish time.
{
"type": "event",
"channel": "workspace:WORKSPACE_ID",
"event": "deployment.updated",
"data": {
"deployment_id": "DEPLOYMENT_ID",
"promotion_id": "DEPLOYMENT_ID",
"model_id": "MODEL_ID",
"status": "ready"
},
"ts": "2026-07-15T12:03:41+00:00"
}error
Something about a frame you sent was rejected. The connection stays
open. channel and id appear only when they apply.
{
"type": "error",
"code": "forbidden",
"message": "no access to this workspace",
"channel": "workspace:WORKSPACE_ID",
"id": "c1"
}code | When |
|---|---|
bad_request | Invalid JSON, a non-object frame, a missing channel, or an unknown action |
forbidden | The channel is malformed, unknown, or you're not authorized for it — see Channels |
limit_exceeded | You hit the 50-channel per-connection cap |
pong
The reply to your ping, echoing your id.
{ "type": "pong", "id": "c3", "ts": "2026-07-15T12:00:25+00:00" }Delivery semantics
- Best-effort. An event can occasionally be lost between the platform and your socket. REST remains the source of truth — treat events as invalidation hints, not as the state itself.
- No replay. Events fired while you were disconnected are gone. After reconnecting, resubscribe and re-fetch current state via REST.
- Slow consumers are dropped. If your client stops reading and falls
too far behind, the gateway closes the connection (code
1011) instead of buffering without bound. Reconnect and reconcile. - Ordering is preserved per channel in normal operation, but don't
build logic that breaks if two events arrive swapped — use the
tsfield and REST reads to settle races.

