WebSocket API Reference
Low-level WebSocket protocol reference for NoLag real-time communication. Frames are shown as JSON for readability; on the wire the server always sends MessagePack binary frames.
Connection
| Property | Value |
|---|---|
| Endpoint | wss://broker.nolag.app/ws |
| Encoding | MessagePack binary frames. The server also accepts JSON text frames from the client, but every reply is MessagePack binary. |
| Heartbeat | Client-driven. Send an empty binary frame (0 bytes); the server echoes it back once you are authenticated. The SDKs send one every 30 seconds. |
| Idle timeout | 60 seconds. A connection that sends nothing (heartbeats included) for 60 seconds is closed by the server. |
| Max frame size | 1 MB. The payload ceiling is 900 KB (921,600 bytes); see Error Reference. |
Authentication
The first message sent after connecting must be an auth request. Any other frame sent before authentication is answered with { "type": "error", "error": "not_authenticated" }. The server sets no separate authentication deadline; only the 60-second idle timeout applies. The SDKs give up on their side if no auth reply arrives within 10 seconds.
{
"type": "auth",
"token": "at_live_<key_id>.<secret>",
"protocolVersion": 2,
"reconnect": false,
"clientId": "worker-7",
"projectId": "..."
}Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "auth" |
token | string | Yes | Actor access token (at_live_<key_id>.<secret>) or a client token (JWT) |
protocolVersion | number | No | Send 2. Absent means 1 (legacy): no 42940 unknown_topic frames, no hint, no published acks. The server replies with the negotiated version, min(client, server). |
reconnect | boolean | No | true restores the subscriptions recorded for this actor and returns them in restoredSubscriptions. Default false: a fresh connection starts with no subscriptions. |
clientId | string | No | Names this client instance for persistent sessions (agent and orchestrator actors). Without it, only the first live connection for the actor keeps a resumable session; a second connection with the same token takes the session over. |
projectId | string | No | Used only for pre-auth logging. Authorization comes from the token. |
Success response fields: actorTokenId, projectId, actorType, the negotiated protocolVersion, and restoredSubscriptions (empty unless reconnect was true). Each restored entry is { "pattern", "topic", "load_balance", "load_balance_group", "filters" }, where pattern is the app/room/topic address and topic is the broker's internal topic.
On failure the socket stays open; error is one of the strings listed under Authentication Errors.
Client to Server Messages
1. subscribe
Subscribe to a topic to begin receiving messages. Idempotent: subscribing again with different options switches the subscription in place.
{
"type": "subscribe",
"topic": "app/room/topic",
"qos": 1,
"filters": ["f1", ["region:eu", "tier:gold"]],
"loadBalance": false,
"loadBalanceGroup": "group-1"
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "subscribe" |
topic | string | Yes | Full address app-slug/room-slug/topic. The app slug is the suffixed value returned when the app was created. |
qos | number | No | 0, 1 (default), or 2. Passed to the broker's internal MQTT hop; see QoS Levels. |
filters | (string | string) | No | Max 100 entries. A string entry matches publishes carrying that filter; a nested array is an AND-group (2 or more values, all of which must be on the publish). Values may not contain /, #, + or |, and may not be empty. Omit or send [] to receive every message on the topic, filtered or not. |
loadBalance | boolean | No | true shares the topic's messages across the members of loadBalanceGroup instead of delivering each message to every subscriber. |
loadBalanceGroup | string | No | Group name for load balancing, scoped to the app. Defaults to this actor's actorTokenId, so two connections of the same actor form a group by default. |
The room must already exist; an unknown room, or one this actor may not access, returns 42940 unknown_topic. See Rooms.
2. unsubscribe
Stop receiving messages from a topic.
{
"type": "unsubscribe",
"topic": "app/room/topic"
}3. publish
Publish a message to a topic.
{
"type": "publish",
"topic": "app/room/topic",
"data": { "your": "payload" },
"qos": 1,
"echo": true,
"retain": false,
"filter": "f1",
"msgRef": "17-k3j9x2"
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "publish" |
topic | string | Yes | Full address app-slug/room-slug/topic |
data | any | Yes | The message payload (any MessagePack-encodable value, up to 900 KB) |
qos | number | No | 0, 1 (default), or 2 |
echo | boolean | No | Default true. Accepted on every publish, but the publishing actor never receives its own message, on any of its connections, whatever the value; append your own message locally in the UI. A process that needs to observe an actor's publishes must connect with its own actor token. |
retain | boolean | No | Default false. true keeps the message as the topic's last value so a later subscriber receives it on subscribe. Retained values are held in broker memory and do not survive a broker restart. |
filter | string | No | Route the message only to subscribers whose filter set includes this value. Subscribers with no filters receive it too. |
filters | string | No | Alternative to filter: a set of values matched against subscribers' AND-groups. |
msgRef | string | No | Client-chosen reference. When present, the server answers with a published frame carrying the same msgRef, or an error frame carrying it if the publish was rejected. Requires protocol v2. |
Publishing is limited to 50 messages per second per connection (42910 rate_limit_exceeded).
4. setFilters
Replace the entire filter set for a subscription. Sending an empty array acts as a wildcard, meaning all messages will be received. Filter values follow the same rules as on subscribe. Sending this for a topic you have not subscribed to returns not_subscribed.
{
"type": "setFilters",
"topic": "app/room/topic",
"filters": ["f1"]
}5. presence
Announce or update presence data for a room. Presence is room-scoped; roomId is the room slug and must be a room the actor can access. Sending the frame again for a different room leaves the previous room first.
{
"type": "presence",
"roomId": "room-slug",
"data": { "status": "online" }
}A presence frame without roomId is deprecated: the server stores the data on the connection but broadcasts nothing.
6. getPresence
Request the current presence list for a room.
{
"type": "getPresence",
"roomId": "room-slug"
}7. ack
Acknowledge receipt of a single message. Send it when the server delivers a message with requiresAck: true.
{
"type": "ack",
"msgId": "uuid"
}8. batchAck
Acknowledge receipt of multiple messages at once.
{
"type": "batchAck",
"msgIds": ["id1", "id2"]
}9. lobbySubscribe
Subscribe to a lobby to receive aggregated presence events across multiple rooms. A lobby the actor may not access returns { "type": "error", "error": "not_authorized", "lobbyId": "..." }.
{
"type": "lobbySubscribe",
"lobbyId": "lobby-slug"
}10. lobbyUnsubscribe
Unsubscribe from a lobby.
{
"type": "lobbyUnsubscribe",
"lobbyId": "lobby-slug"
}11. getLobbyPresence
Request the current presence data for an entire lobby.
{
"type": "getLobbyPresence",
"lobbyId": "lobby-slug"
}12. reauth
Renew a client token on the live connection, without disconnecting or resubscribing. The new token must resolve to the same actor as the current connection. On success the connection's grants, limits and expiry are refreshed from the new token.
{
"type": "reauth",
"token": "<new client token>"
}authExpiresAt (Unix seconds) is present when the new token carries an expiry. On failure the connection stays open under its current authentication; error is actor_mismatch, token_expired, or one of the authentication error strings.
Server to Client Messages
1. message
Delivers a published message to the client.
{
"type": "message",
"topic": "app/room/topic",
"data": { "your": "payload" },
"msgId": "uuid",
"requiresAck": true,
"isReplay": false,
"filter": "f1"
}| Field | Type | Description |
|---|---|---|
topic | string | The app/room/topic address the message was published to |
data | any | The published payload |
msgId | string | Present when message recording is enabled on the broker (a broker-wide setting, not a per-topic one). Absent otherwise. |
requiresAck | boolean | Sent as true alongside msgId; reply with ack or batchAck |
isReplay | boolean | true on messages delivered by a load-balanced group replay; see Replay. Absent on ordinary live delivery. |
filter | string | The filter value the message was published with, when it had one |
2. subscribed
Confirms a successful subscription.
{
"type": "subscribed",
"topic": "app/room/topic",
"loadBalance": false
}3. unsubscribed
Confirms a successful unsubscription.
{
"type": "unsubscribed",
"topic": "app/room/topic"
}4. published
Acknowledges a publish that carried a msgRef (protocol v2). The SDKs surface this as the publish callback. There is no resend on the WebSocket leg; a publish that is never acknowledged has to be repeated by the client.
{
"type": "published",
"topic": "app/room/topic",
"msgRef": "17-k3j9x2"
}5. filtersUpdated
Confirms that the filter set for a subscription has been updated.
{
"type": "filtersUpdated",
"topic": "app/room/topic",
"filters": ["f1"]
}6. presence
Notifies the client of a presence change in a room it has announced presence in. Note the snake_case keys, which differ from presenceList.
{
"type": "presence",
"event": "join",
"data": {
"actor_token_id": "...",
"presence": { "status": "online" }
}
}The event field is one of "join", "leave", or "update". Events carry no joinedAt; that field is only on presenceList entries. The SDKs normalise actor_token_id to actorTokenId before handing the event to your code.
7. presenceList
Returns the full list of present actors in a room. Sent in response to a getPresence request.
{
"type": "presenceList",
"roomId": "room-slug",
"data": [
{ "actorTokenId": "...", "presence": { "status": "online" }, "joinedAt": 1716000000 }
]
}joinedAt is Unix seconds. A getPresence without roomId is deprecated and returns an empty data array.
8. lobbySubscribed
Confirms a lobby subscription and includes the current presence snapshot. The snapshot is keyed by room slug, then by actorTokenId; each inner value is a full presence entry.
{
"type": "lobbySubscribed",
"lobbyId": "lobby-slug",
"presence": {
"general": {
"019fd987-a03a-70cf-9420-60538a186bbb": {
"actorTokenId": "019fd987-a03a-70cf-9420-60538a186bbb",
"presence": { "status": "online" },
"joinedAt": 1716000000
}
}
}
}9. lobbyUnsubscribed
Confirms a lobby unsubscription.
{
"type": "lobbyUnsubscribed",
"lobbyId": "lobby-slug"
}10. lobbyPresence
Notifies the client of a presence event within a lobby. roomId is the room slug; data is the actor's presence payload (empty on leave).
{
"type": "lobbyPresence",
"event": "join",
"lobbyId": "lobby-slug",
"roomId": "general",
"actorId": "019fd987-a03a-70cf-9420-60538a186bbb",
"data": { "status": "online" }
}11. lobbyPresenceList
Returns the full presence data for a lobby, in the same shape as the lobbySubscribed snapshot. Sent in response to a getLobbyPresence request.
{
"type": "lobbyPresenceList",
"lobbyId": "lobby-slug",
"presence": {
"general": {
"019fd987-a03a-70cf-9420-60538a186bbb": {
"actorTokenId": "019fd987-a03a-70cf-9420-60538a186bbb",
"presence": { "status": "online" },
"joinedAt": 1716000000
}
}
}
}12. replayStart
Marks the beginning of a replay. Replay only happens for a load-balanced worker group with a persistent session that scaled to zero and is being woken; it does not happen on an ordinary reconnect. See Replay.
{
"type": "replayStart",
"count": 247
}Live messages that arrive during a replay are buffered and delivered after replayEnd, deduplicated against the replayed ids.
13. replayEnd
Marks the end of a replay.
{
"type": "replayEnd",
"replayed": 247
}14. hydration
Sent once per subscribe when a webhook is configured for the topic, either topicConfigs.<topic>.webhooks.onSubscribe or the app-level hydrationWebhook (the per-topic entry wins). topic is the bare topic name, not the full address; data is whatever the webhook returned. Hydration is best-effort: a failing webhook is logged and no frame is sent. See Webhooks.
{
"type": "hydration",
"topic": "messages",
"data": { "initialState": "..." }
}15. error
Reports an error to the client. Fields and codes are listed in the Error Reference.
{
"type": "error",
"error": "unknown_topic",
"code": 42940,
"topic": "app/room/topic",
"hint": "room is not configured. Provision it via the control-plane rooms API before use."
}16. disconnect
Informs the client that the server is about to close the connection. Sent only before a 4003 close, when a client token has expired; other server-initiated closes (4001, 4002) carry their reason in the close frame instead.
{
"type": "disconnect",
"reason": "token_expired"
}QoS Levels
qos on subscribe and publish is validated (0, 1, 2; anything else becomes 1) and passed to the broker's internal MQTT hop. It does not change what happens on the WebSocket leg, where the only delivery signal is the optional published ack and there is no resend.
| Level | Name | Description |
|---|---|---|
0 | At most once | Fire and forget on the internal hop. |
1 | At least once | Default. The internal hop acknowledges delivery; a message may arrive more than once. |
2 | Assured on the internal hop | The MQTT hop uses its two-step handshake. This gives your client no end-to-end deduplication: duplicates are still possible across a reconnect, and a publish is not resent if its published ack is lost. Deduplicate on msgId or your own message ids when it matters. |
See QoS.
Reconnection
When a client reconnects after a disconnect, it sends the auth message with reconnect: true. The server restores the subscriptions it recorded for the actor (topic, filters, load-balance settings) and returns them in restoredSubscriptions. Nothing is replayed on an ordinary reconnect: messages published while the client was offline are gone unless the client is a member of a load-balanced worker group with a persistent session, in which case the replay flow runs.
Reconnection flow
- Client reconnects and sends
authwithreconnect: true(and the sameclientId, if it uses one). - Server responds with
restoredSubscriptions, an array of{ "pattern", "topic", "load_balance", "load_balance_group", "filters" }objects, and resubscribes the connection to each. - Live delivery resumes.
- For a woken load-balanced group only:
replayStart, the replayed messages withisReplay: true, thenreplayEnd.
Backoff strategy
The SDKs use this schedule; a hand-rolled client should do something similar.
| Parameter | Value |
|---|---|
| Base delay | 5 seconds |
| Max delay | 30 seconds |
| Multiplier | 1.5x |
Each failed reconnection attempt increases the delay by the multiplier, up to the maximum. For example: 5s, 7.5s, 11.25s, 16.875s, 25.3s, 30s, 30s, ...