Protocol reference
WebSocket Protocol Reference
Build a pubb integration with your runtime’s WebSocket and HTTP clients. This reference describes the messages on the wire, including the exact control event names and payload types.
Connection
wss://ws.pubb.io/socket?appKey=YOUR_APP_KEY&protocol=2
Open a standard WebSocket connection over TLS. URL-encode the public app key; the app ID and app secret do not belong in this URL. No custom headers or WebSocket subprotocol are required.
After the HTTP upgrade, wait for this connection event before subscribing. Parse data as JSON to read your socket_id and the activity_timeout in seconds.
{
"event": "pubb:connection_established",
"data": "{\"socket_id\":\"0123456789abcdef0123456789abcdef\",\"activity_timeout\":60}"
}
Each connection receives a new socket ID. Use it to authorize private or presence subscriptions and, optionally, exclude this connection when publishing.
Message format
Send one JSON object per WebSocket text message. Commands use action; server control messages use event. Published application events arrive in a channel envelope with no top-level event.
| Server message | Payload type |
|---|---|
pubb:connection_established | data is a JSON string with socket_id and activity_timeout. |
pubb:subscription_succeeded | Public/private: the string "{}". Initial presence subscription: an object with presence. |
pubb:member_added / pubb:member_removed | data is an object. See presence examples below. |
subscription_error / error | data is a plain error string, not JSON. |
Application event (no top-level event) | data is a JSON string containing name and data. |
Parse the outer message first, then decode its payload according to its type. Use the exact control names above: SDK-facing aliases are not replacements for wire event names. Ignore unknown control events for forward compatibility.
Use protocol=2 in the connection URL to receive pubb:subscription_succeeded. Connections without this parameter retain the legacy acknowledgement for existing SDKs. The server sends exactly one acknowledgement per subscribe command, with the same payload in either mode.
Authentication
The public app key identifies the application. Anyone with that key can subscribe to public channels. Channels beginning with private- or presence- require a signature from your trusted backend. Channel access is isolated by application.
- Read
socket_idfrom the connection event. - Send
socket_idandchannel_nameto an authenticated endpoint in your own backend. - Your backend checks the user’s access to that exact channel, then calls the pubb authorization endpoint below or signs locally.
- Return
authto the client. For presence, also returnchannel_dataunchanged.
POST https://api.pubb.io/api/apps/YOUR_APP_ID/auth
Authorization: Bearer YOUR_APP_SECRET
Content-Type: application/json
{
"socket_id": "0123456789abcdef0123456789abcdef",
"channel_name": "private-user-42"
}
{
"auth": "YOUR_APP_KEY:LOWERCASE_HEX_HMAC_SHA256"
}
For a presence channel, include a nonempty string user_id and optional object user_info in the authorization request. Derive both from the authenticated user on your server.
{
"socket_id": "0123456789abcdef0123456789abcdef",
"channel_name": "presence-team-42",
"user_id": "42",
"user_info": {
"name": "Alex"
}
}
{
"auth": "YOUR_APP_KEY:LOWERCASE_HEX_HMAC_SHA256",
"channel_data": "{\"user_id\":\"42\",\"user_info\":{\"name\":\"Alex\"}}"
}
Signing without the authorization API
Compute HMAC-SHA256 on your server using the app secret as the key and the UTF-8 string below as input. Encode the digest as lowercase hexadecimal, then return APP_KEY:signature.
Private: socket_id:channel_name
Presence: socket_id:channel_name:channel_data
Sign the exact serialized channel_data string that you send. Reformatting its JSON changes the signature. Never expose the app secret to a browser or distributed client.
Subscribe / Unsubscribe
{
"action": "subscribe",
"channel": "notifications"
}
{
"action": "subscribe",
"channel": "private-user-42",
"auth": "YOUR_APP_KEY:LOWERCASE_HEX_HMAC_SHA256"
}
{
"action": "subscribe",
"channel": "presence-team-42",
"auth": "YOUR_APP_KEY:LOWERCASE_HEX_HMAC_SHA256",
"channel_data": "{\"user_id\":\"42\",\"user_info\":{\"name\":\"Alex\"}}"
}
action and channel are required strings. Restricted subscriptions also require the auth string; presence requires channel_data as a string, not an object. These examples use placeholders for signatures.
{
"event": "pubb:subscription_succeeded",
"channel": "notifications",
"data": "{}"
}
Wait for the acknowledgement before expecting delivery. Repeating a valid subscription does not add another subscription; it returns the same acknowledgement with string "{}", even for an already subscribed presence channel. It does not return a fresh presence snapshot.
{
"action": "unsubscribe",
"channel": "notifications"
}
Unsubscribe has no acknowledgement and does not require a signature. Remove the channel from your client’s desired subscriptions and ignore any messages already in flight. Closing a connection removes all its subscriptions.
Presence events
A new presence subscription returns an object containing unique user IDs, a map from user ID to user info, and the member count.
{
"event": "pubb:subscription_succeeded",
"channel": "presence-team-42",
"data": {
"presence": {
"ids": [
"42"
],
"hash": {
"42": {
"name": "Alex"
}
},
"count": 1
}
}
}
{
"event": "pubb:member_added",
"channel": "presence-team-42",
"data": {
"user_id": "43",
"user_info": {
"name": "Sam"
}
}
}
{
"event": "pubb:member_removed",
"channel": "presence-team-42",
"data": {
"user_id": "43"
}
}
Multiple sockets with the same user ID count as one member. Joining with another socket does not add a second member; removal is emitted when the user’s last socket leaves. Rebuild your member list from the new subscription snapshot after reconnecting.
Publish an event over HTTP
The WebSocket accepts subscription commands only. There is no WebSocket publish action or client-event command. Publish from your trusted backend after checking the caller’s permission.
POST https://api.pubb.io/api/apps/YOUR_APP_ID/events
Authorization: Bearer YOUR_APP_SECRET
Content-Type: application/json
Idempotency-Key: order-1042-created
{
"channel": "notifications",
"name": "order.created",
"data": { "orderId": "1042" }
}
{
"success": true,
"accepted": true,
"channels": [
"notifications"
],
"publicationId": "11111111-2222-4333-8444-555555555555",
"duplicate": false
}
channel is a string or an array of 1–100 strings. Event and channel names must be nonblank and at most 200 characters; publishing rejects channels beginning with debug- or _. Use ASCII channel names of at most 200 bytes to also fit the WebSocket limit. data can be any JSON value and is limited to 10 KiB of UTF-8 JSON; omitted or null data is sent as null.
Optional socket_id excludes one connection. The optional Idempotency-Key header accepts 1–128 printable, nonspace ASCII characters. Reuse it for retries of the same publication within the 24-hour retention window; a repeat returns duplicate: true and the original publication ID.
{
"channel": "notifications",
"data": "{\"name\":\"order.created\",\"data\":{\"orderId\":\"1042\"}}"
}
Decode the outer data string once to obtain the application event name and payload. The publication ID is not included in this WebSocket envelope. Add your own event ID inside the payload if your application needs to identify events.
HTTP 202 confirms acceptance for processing, not delivery. Publications target subscribers connected at acceptance. There is no client delivery acknowledgement, replay cursor or missed-event history; refresh durable state from your backend after interruptions.
Ping / Pong heartbeat
The server sends a WebSocket Ping control frame approximately every 20 seconds. Your WebSocket implementation must respond with a Pong control frame. The read deadline starts at 60 seconds and is extended by 60 seconds whenever a Pong is received; a missed deadline closes the connection.
Browsers handle these frames automatically; JavaScript does not receive them as message events. In other runtimes, enable your library’s automatic Pong handling or respond to Ping with the same payload. Application messages do not extend the Pong deadline.
There is no JSON heartbeat command. Sending {"action":"ping"} or an event named pubb:ping does not satisfy the heartbeat. The initial activity_timeout: 60 describes the server’s timeout in seconds.
Reconnection
Reconnection is client behavior. A native WebSocket does not reconnect automatically, and the server does not restore an old connection or its subscriptions.
- On an unexpected close, schedule a bounded retry with exponential backoff and jitter. For example, start around one second and cap the delay at 30 seconds.
- Open a new connection and wait for its connection-established event. Discard the old socket ID and pending authorization requests.
- Obtain fresh signatures for every private or presence channel using the new socket ID, then resubscribe to all desired channels.
- Wait for each subscription acknowledgement and refresh durable application state from your backend. Spread large batches of subscriptions to stay within 20 commands per second.
- Cancel retries and pending authorization work when the user intentionally disconnects or the component is disposed. Avoid duplicate sockets and event handlers.
The bundled JavaScript SDK retries up to five times with delays of 2, 4, 8, 16 and 30 seconds, resets the retry counter after the connection-established event, and resubscribes. Those are SDK defaults, not a server requirement. A custom client should choose a retry policy appropriate to its runtime.
Honor Retry-After when the runtime exposes handshake responses. Fix credentials or invalid subscription data before retrying permanent errors. Browser WebSocket APIs generally expose a generic error/close instead of the rejected HTTP response body, so keep retries bounded and surface connection failure.
Errors and limits
Connection handshake errors
These are HTTP responses before the WebSocket upgrade, with a plain-text body. They are not WebSocket JSON events.
| HTTP | Body | Handling |
|---|---|---|
| 400 | appKey is required | Include the public app key in the URL. |
| 401 | Invalid application key | Check the key and application. Keys longer than 64 bytes are rejected. |
| 429 | Workspace connection limit reached | Retry-After: 5. Wait at least five seconds and use backoff. |
| 503 | Workspace capacity is temporarily unavailable | Retry with backoff. |
| 503 | Connection unavailable | Retry with backoff. |
Subscription errors
{
"event": "subscription_error",
"channel": "private-user-42",
"data": "Subscription not authorized"
}
A rejected subscription leaves the connection open. There is no numeric error code in this envelope.
| data | Meaning / handling |
|---|---|
Channel unavailable | Empty channel, channel longer than 200 UTF-8 bytes, reserved debug- prefix, or unavailable application secret. |
Valid presence identity required | channel_data must be a JSON string containing a nonempty string user_id. |
Subscription not authorized | Missing or invalid signature. Authorize this channel using the current socket_id. |
Maximum 100 channels per connection | Unsubscribe from another channel before adding one. |
Subscription temporarily unavailable | Temporary subscription storage failure. Retry with backoff. |
Connection limits and malformed commands
A connection accepts at most 100 channels and client messages of at most 16 KiB. More than 20 commands in a one-second window causes disconnection; the server attempts to send the error below before closing, but it may not reach the client.
{
"event": "error",
"data": "Command rate exceeded (20/s)"
}
Malformed JSON or invalid command field types are ignored without a response. Unknown or missing actions are also ignored. They still consume the command rate allowance. Do not wait indefinitely for an acknowledgement; use a bounded subscription timeout.
Oversized WebSocket messages can produce close code 1009 (message too big). Heartbeat timeouts, slow readers and transport failures may appear as an abnormal close, such as 1006, or a close without a reason. pubb does not define custom application close codes; do not infer a subscription error from a close code alone.
HTTP publishing and authorization errors
API errors use JSON. Publishing validation and capacity errors include error and code; authentication and authorization validation responses may contain only error.
{
"error": "Workspace throughput is busy. Retry after the indicated delay.",
"code": "rate_limit_exceeded"
}
| HTTP | code | Handling |
|---|---|---|
| 400 | invalid_event | Supply an event name and 1–100 valid channel names. |
| 400 | invalid_socket | socket_id must be a string of at most 100 characters. |
| 400 | invalid_idempotency_key | Use 1–128 printable, nonspace ASCII characters. |
| 409 | idempotency_conflict | The key was already used for a different event or publication source. |
| 413 | payload_too_large | Event data exceeds 10 KiB of UTF-8 JSON. |
| 413 | body_too_large | The entire HTTP request body exceeds 64 KiB. |
| 429 | rate_limit_exceeded | Wait for Retry-After before retrying with the same idempotency key. |
| 429 | fanout_too_large | Reduce recipients or increase plan capacity; an unchanged retry cannot fix this. |
| 429 | monthly_quota_exceeded | Wait for the next UTC calendar month or upgrade the workspace. |
| 503 | capacity_unavailable | Retry with backoff and the same idempotency key. |
Both endpoints return HTTP 401 with A valid application secret is required for an invalid secret, or HTTP 404 with Application not found for an unknown app. Authorization returns HTTP 400 with one of these error strings:
socket_id and channel_name are requiredChannel does not require authenticationuser_id is required for presence channels
For transient HTTP failures, retry with backoff. Reuse the publication’s idempotency key after an ambiguous timeout so a retry does not create another publication.
Minimal browser example
This example connects, waits for the handshake, subscribes and decodes application events without an SDK. Publish the sample event from your backend after the subscription acknowledgement.
const channel = "notifications";
const url = new URL("wss://ws.pubb.io/socket");
url.searchParams.set("appKey", "YOUR_APP_KEY");
url.searchParams.set("protocol", "2");
const socket = new WebSocket(url);
socket.onmessage = ({ data }) => {
try {
const packet = JSON.parse(data);
if (packet.event === "pubb:connection_established") {
const { socket_id } = JSON.parse(packet.data);
console.log("Connected", socket_id);
socket.send(JSON.stringify({ action: "subscribe", channel }));
return;
}
if (packet.event === "error" || packet.event === "subscription_error") {
console.error(packet.event, packet.channel, packet.data);
return;
}
if (packet.channel !== channel) return;
if (packet.event === "pubb:subscription_succeeded") {
console.log("Ready to receive events");
return;
}
if (packet.event) return; // Other control events
const event = JSON.parse(packet.data);
console.log(event.name, event.data);
} catch (error) {
console.error("Could not read the packet", error);
}
};
socket.onerror = () => console.error("WebSocket connection failed");
socket.onclose = ({ code, reason }) => console.log("Disconnected", code, reason);
function unsubscribe() {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ action: "unsubscribe", channel }));
}
}
// On component/page disposal, stop any reconnect timers and close the socket:
function dispose() {
socket.close(1000, "Client disposed");
}
Add the reconnection policy above for production. In React, create the socket in an effect and call dispose() from its cleanup. For private or presence channels, complete the authorization flow before sending the subscription.
For stack-specific helpers, see the SDK guides. Coding assistants can start with the AI integration guide and the downloadable plain-text integration reference.