Pingerchips LogoPingerchips
Wire Protocol

Wire Protocol

The exact bytes on the wire — every WebSocket topic, message, HTTP endpoint, and error, across all four products.

This is the ground-truth reference for building an SDK, debugging a raw socket frame, or implementing a client in a language Pingerchips doesn't ship. It describes what the server actually sends and accepts today — not a roadmap.

The official SDKs (pingerchips-js, pingerchips-js-server, @pingerchips/ai, pingerchips-python) wrap all of this. Read this only when you need the layer underneath.


Transport

All real-time traffic is Phoenix Channels over a single WebSocket. All server-to-server traffic is HTTP with X-App-Key + X-App-Secret headers (Authentication).

EnvironmentWebSocketHTTP
Productionwss://queue.pingerchips.com/sockethttps://queue.pingerchips.com
Developmentws://localhost:4000/sockethttp://localhost:4000

Socket connection

Phoenix's socket handshake carries params in the query string:

wss://queue.pingerchips.com/socket/websocket?app_key={appKey}&vsn=2.0.0
ParamRequiredMeaning
app_keyyesidentifies the app; the connection is rejected (:error, socket closes) if unknown, over the connection limit, or from a disallowed Origin
client_idnoa default client id, used by channels that don't get one from a token
vsnyesPhoenix wire version — use 2.0.0

There is no auth on the socket itself beyond app_key + Origin. Authorization happens per-channel, in join.

The Phoenix message frame

Every frame — v2.0.0 wire format — is a 5-element JSON array:

["{join_ref}", "{ref}", "{topic}", "{event}", {payload}]
SlotMeaning
join_refthe ref of the phx_join that opened this channel; null for messages not tied to a join
refa client-chosen id echoed in the matching reply; null for server-initiated pushes
topicthe channel topic (see each product below)
eventthe message name
payloadan arbitrary JSON object

Joining a channel:

["1", "1", "app:pk_live_x:room:orders", "phx_join", { "auth": "..." }]

Reply (event is always phx_reply, ref matches):

["1", "1", "app:pk_live_x:room:orders", "phx_reply", { "status": "ok", "response": { "socket_id": "..." } }]

status is "ok" or "error"; the channel-specific body is under response.

Heartbeat — send every ~30 s or Phoenix closes the socket:

[null, "hb-1", "phoenix", "heartbeat", {}]

Topics at a glance

Topic patternChannelProduct
app:{appKey}:room:{channel}PubSubpub/sub messaging
durable:{appKey}:{type}:{key}Durable Objectsreplicated state
chat:v1:app:{appKey}:thread:{threadId}ChatAI chat sessions

Spaces has no topic of its own — it runs on a PubSub room: channel (today, one named ephemeral-{spaceId}). See Spaces.

The version segment (v1 for chat, none yet for the others) is frozen. A breaking change bumps it — chat:v2:... — and both versions run on one socket. See Versioning.


Authentication summary

PathAuth
WebSocket connectapp_key query param + Origin check
PubSub public channel joinnone, unless the app has enable_user_authentication = 1
PubSub private / presence channel join{ auth: "<capability token>" } granting channel:subscribe:{channel}
Durable Object channel join (browser){ auth: "<capability token>" } granting object:read:{type}/{key}
Durable Object channel join (server){ app_secret: "..." }
Chat thread join (client){ auth: "<capability token>" } granting chat:subscribe:{threadId}
Chat thread join (agent){ secret: "...", agent_id: "..." }
Every server HTTP callX-App-Key + X-App-Secret headers
Capability-token issuer (POST /api/auth)X-App-Key + X-App-Secret headers

Full model, token format, and capability grammar: Authentication.


Pages

  • PubSubapp:* channels: messages, delta compression, replay, client publish
  • Durable Objectsdurable:* channels + the /api/v1/objects REST API
  • Spaces — cursors, member presence, locations, locks
  • Chatchat:v1:* channels: run lifecycle, token streaming, the conversation tree
  • Errors & sequencing — every error code, and the ordering guarantees
  • Versioning — what counts as a breaking change

On this page