PieSocket Protocol V3 to V4
A guide for moving from PieSocket's V3 protocol to V4. V3 keeps running alongside V4, so you can migrate on your own schedule, but new clients should build against V4 directly.
If you're using one of our official SDKs, see Migrating an SDK client below, it's a one-line change. If your backend publishes messages or reads presence directly via HTTP, see Migrating your backend. If you use webhooks, see Webhooks. If you built your own client directly against the WebSocket wire protocol, see Migrating a custom client for the full message-format changes.
Migrating an SDK client
Every official SDK added V4 support the same way: a version option on the client constructor, defaulting to 3. Set it to 4 and reconnect, the rest of your code (subscribing, listening, publishing) stays the same.
There's no separate method for multiplexing either. Whatever method you already use to join a channel (subscribe() in JavaScript, join() elsewhere) opens the connection the first time and, on V4, transparently rides the same connection for every channel after that:
import PieSocket from 'piesocket-js';
const piesocket = new PieSocket({
version: 4, // new, defaults to 3
clusterId: 'YOUR_CLUSTER_ID',
apiKey: 'YOUR_API_KEY',
});
const room1 = await piesocket.subscribe('room-1'); // opens the socket
const room2 = await piesocket.subscribe('room-2'); // rides the same socket
room1.listen('message', (data) => { /* ... */ });
room2.publish('message', { text: 'hi' });
PieSocketOptions options = new PieSocketOptions();
options.setClusterId("YOUR_CLUSTER_ID");
options.setApiKey("YOUR_API_KEY");
options.setVersion("4"); // new, defaults to "3"
PieSocket piesocket = new PieSocket(options);
Channel room1 = piesocket.join("room-1"); // opens the socket
Channel room2 = piesocket.join("room-2"); // rides the same socket
room1.listen("message", event -> { /* ... */ });
room2.publishEvent("message", new JSONObject().put("text", "hi"), null);
PieSocketOptions options = PieSocketOptions();
options.setClusterId("YOUR_CLUSTER_ID");
options.setApiKey("YOUR_API_KEY");
options.setVersion("4"); // new, defaults to "3"
PieSocket piesocket = PieSocket(options);
Channel room1 = piesocket.join("room-1"); // opens the socket
Channel room2 = piesocket.join("room-2"); // rides the same socket
room1.listen("message", (event) { /* ... */ });
room2.publishEvent("message", data: {"text": "hi"});
let options = PieSocketOptions()
options.setClusterId(clusterId: "YOUR_CLUSTER_ID")
options.setApiKey(apiKey: "YOUR_API_KEY")
options.setVersion(version: "4") // new, defaults to "3"
let piesocket = PieSocket(pieSocketOptions: options)
let room1 = piesocket.join(roomId: "room-1") // opens the socket
let room2 = piesocket.join(roomId: "room-2") // rides the same socket
room1.listen(eventName: "message") { event in /* ... */ }
room2.publishEvent("message", data: ["text": "hi"])
A few other things change under the hood once version: 4 is set, all handled by the SDK, no code changes needed beyond what's shown above:
- Presence becomes delta-based: you get the full roster once, then incremental join/leave updates deduplicated by identity, rather than the whole roster resent on every change. A new
refreshMembers()method re-syncs it on demand. - A
publishEvent(event, data, meta)-style method is available alongsidepublish(), useful if you were working around double-JSON-encoding on the old method. - A
sendBinary()method is available for raw binary payloads on the primary channel.
Migrating your backend
If your backend publishes messages via the Publish API instead of, or in addition to, a WebSocket connection, point it at the V4 path. The request body and response are identical, only the path changes:
POST https://CLUSTER_ID.piesocket.com/api/publish ← V3
POST https://CLUSTER_ID.piesocket.com/api/v4/publish ← V4
{
"key": "YOUR_API_KEY",
"secret": "YOUR_API_SECRET",
"roomId": "room-1",
"message": { "event": "new-message", "data": "Hello" }
}
If your backend also queries presence membership over HTTP (rather than from a WebSocket connection), that moves too, and the response tightens up on V4:
POST https://CLUSTER_ID.piesocket.com/api/members ← V3
POST https://CLUSTER_ID.piesocket.com/api/v4/members ← V4
roomIdis optional on V3, omit it to get every room for that API key grouped by name, it's required on V4.- V4's response adds a
countfield and deduplicates members by identity, the same presence change covered below. V3's response is a flat array with no dedup.
Webhooks
Nothing to reconfigure on the webhook itself, the same endpoint URL keeps working. What changes is the payload shape, and it follows the publishing connection's protocol version, not your cluster as a whole:
V3 sends one unbatched request per message:
{
"apiKey": "your-api-key",
"channel": "room-1",
"message": "Hello world"
}
V4 batches messages, a single request can carry up to 250 of them:
{
"apiKey": "your-api-key",
"messages": [
{ "channel": "room-1", "message": "Hello world", "timestamp": 1732000000000 }
]
}
If you migrate gradually, some of your clients on V3 and some already on V4, the same webhook URL receives both shapes at once, interleaved, since batching is decided per message rather than per cluster. Your endpoint needs to handle both for the duration of the rollout, not just the shape you're migrating to. See Payload format for both shapes in full.
Migrating a custom client
The rest of this guide is for clients built directly against the raw WebSocket protocol, without an SDK. See 3rd Party Clients if that's new to you.
1. Swap the connection path
Everything else about the URL stays the same, just change /v3/ to /v4/:
wss://CLUSTER_ID.piesocket.com/v3/CHANNEL_ID?api_key=API_KEY ← V3
wss://CLUSTER_ID.piesocket.com/v4/CHANNEL_ID?api_key=API_KEY ← V4
api_key, notify_self, user, uuid, jwt, and presence all mean the same thing on both versions.
2. Update event names
V4 uses a double colon for every system-emitted event, V3 used a single colon. Update whatever string matching or parsing your client does:
| V3 | V4 |
|---|---|
| system:error | system::error |
| system:boot | system::boot |
| system:member_list | system::member_list |
| system:member_joined | system::member_joined |
| system:member_left | system::member_left |
| system:binary | system::binary |
system::to, for addressing a specific user, was already double-colon on V3 and is unchanged.
3. Drop the binary opt-in
V3 required connecting with ?binary=1 (or a binary- prefixed channel name) before binary payloads would be wrapped and delivered correctly. V4 doesn't need any of that, any raw binary frame you send is detected and handled automatically. Remove the binary parameter from your connection URL, it has no effect on V4.
4. Handle the new presence payload shape
V4 presence events add a channel field (needed once a connection can subscribe to more than one channel) and a count field, and membership is deduplicated by identity rather than by connection:
// V3
{ "event": "system:member_joined", "data": { "member": "user-3", "members": ["user-1", "user-2", "user-3"] } }
// V4
{ "event": "system::member_joined", "data": { "channel": "room-1", "member": "user-3", "count": 3 } }
The practical difference: on V3, the same person connected from two tabs shows up as two separate members. On V4, they're deduplicated by uuid (or by user if no uuid was supplied), so member_joined/member_left only fire on that identity's first connection or last disconnection, not on every tab. If your UI was counting raw members array length as "online users," switch to reading the new count field instead.
5. Expect a system::channel field on incoming messages
Every JSON message you receive over V4 has a system::channel field added automatically, naming the channel it was published on:
{ "event": "new-message", "data": "Hello!", "system::channel": "room-1" }
This is new, V3 never added this. If your client does strict schema validation on incoming messages, make sure it tolerates (or ignores) this extra key.
6. Optional: adopt multiplexing
This is the main reason to move to V4, a single connection can now subscribe to more than one channel, instead of reconnecting per channel:
{ "event": "system::subscribe", "data": { "channel": "room-2" } }
You're not required to use this, a V4 connection with just its primary channel (the one from the URL) behaves like a V3 connection in every other respect. See Multiplexing in the V4 reference for the full subscribe/unsubscribe/get_members message formats.
What stays the same
- Message payloads themselves, any string or JSON, no enforced schema
notify_self, client-to-client messaging, and theis_c2c_allowedcluster setting- JWT-based authentication and the
private-channel prefix cmd_statusandcmd_pingbuilt-in commands, and their reply shapes- Message size limits
