Skip to main content

Wire Format

Every message on the wire is a single JSON object with a type field. Both client→server and server→client frames follow the same shape.

The WebSocket endpoint:

wss://rms.metered.ca/v1

Auth is in the query string: ?token=<jwt> for sk-minted JWTs or `?key=<pk_live…>` for browser-shipped publishable keys.

Connection lifecycle

client                          server
│ WS handshake │
│ ── upgrade ?token=…/?key=… ─→ │
│ │ (auth + plan check + quota gate)
│ ←── welcome ──────────────── │
│ │
│ ─── subscribe ────────────→ │
│ ←── ack {requestId} ──────── │
│ ←── presence (joined: ...) ─ │ (peers already in the channel)
│ │
│ ─── publish ──────────────→ │
│ ←── ack {requestId} ──────── │
│ │ (other subscribers see `message`)
│ │
│ ←── going_away ──────────── │ (graceful shutdown only)
│ │
│ ←── WS close-frame (1xxx/4xxx)

Client → server messages

All client messages carry an optional requestId (string, ≤128 chars). When present, the server replies with an ack or error carrying the same requestId so callers can correlate.

subscribe

Join a channel.

{
"type": "subscribe",
"channel": "app_abc/room-1",
"requestId": "sub-1",
"includeSenderMetadata": false
}
  • channel — channel name. MUST match one of the patterns in your key's channelPatterns.
  • includeSenderMetadata — optional, default false. When true, every message you receive on this channel carries the sender's peerMetadata under fromMetadata. See Presence & Metadata.

unsubscribe

{
"type": "unsubscribe",
"channel": "app_abc/room-1",
"requestId": "unsub-1"
}

publish

Broadcast to all other subscribers of a channel.

{
"type": "publish",
"channel": "app_abc/room-1",
"data": { "anything": "json-serializable" },
"requestId": "pub-1"
}

The publisher does NOT receive their own message (matches Pusher / Ably). data can be any JSON value; the server doesn't inspect it beyond byte counting.

send

Direct message to a specific peer (by peerId), no channel required. Used for SDP/ICE, agent-to-agent tool calls, command-and-control.

{
"type": "send",
"to": "bob",
"data": { "kind": "offer", "sdp": "..." },
"requestId": "snd-1"
}

Server replies ack if the peer is connected, error: peer_not_found if they have no active connections. See Authentication for the send permission requirement.

Server → client messages

welcome

Sent immediately after auth succeeds. Always the first message you receive.

{
"type": "welcome",
"peerId": "alice",
"expiresAt": 1715539200,
"serverTime": 1715535600,
"maxMessageSize": 65536,
"metadata": {
"iceServers": [
{ "urls": ["stun:stun.relay.metered.ca:80"] },
{ "urls": ["turn:global.relay.metered.ca:80"], "username": "u", "credential": "c" }
]
}
}
  • peerId — your peer's identity. For JWT auth this is the sub claim; for pk_ auth it's auto-generated by the server.
  • expiresAt — Unix seconds when your JWT expires. The server force-closes your connection ~250ms before this. null for pk_ connections (no expiry).
  • serverTime — server clock at welcome. Use for clock-skew detection.
  • maxMessageSize — hard cap on inbound message bytes (64KB). Larger frames are dropped with close code 1009.
  • metadata — pass-through from the JWT's metadata claim. Conventionally used to ship iceServers for WebRTC. Omitted when empty. See WebRTC Signalling guide.

ack

Acknowledges a successful client request.

{ "type": "ack", "requestId": "sub-1", "ok": true }

error

Reports a client-request failure. The connection STAYS OPEN — error is not a close.

{
"type": "error",
"requestId": "pub-2",
"code": "channel_not_authorized",
"message": "not authorized: other-app/room-1"
}

See Error Codes for the full list.

message

A message published to a channel you're subscribed to.

{
"type": "message",
"channel": "app_abc/room-1",
"from": "bob",
"fromMetadata": { "userId": "u_bob", "username": "Bob" },
"data": { "anything": "json-serializable" }
}
  • from — the publisher's peerId.
  • fromMetadata — present only if you subscribed with includeSenderMetadata: true AND the publisher's JWT carried peerMetadata. See Presence & Metadata.

direct

A peer sent you a send directly (no channel involved).

{
"type": "direct",
"from": "alice",
"fromMetadata": { "userId": "u_alice", "role": "presenter" },
"data": { "kind": "ice-candidate", "candidate": "..." }
}

fromMetadata is always stamped on direct messages when the sender's JWT had peerMetadata (no opt-in required — direct sends are 1:1, so the byte cost of stamping is minimal).

presence

Peer-membership change on a channel you're subscribed to.

{
"type": "presence",
"channel": "app_abc/room-1",
"joined": [
{ "peerId": "carol", "metadata": { "userId": "u_carol", "username": "Carol" } }
],
"left": []
}
  • joined / left — arrays (may be empty). Each entry has peerId plus optional metadata (the peer's peerMetadata JWT claim).
  • A subscribe also produces a one-time presence event listing everyone already in the room before broadcasting your own joined to the others.

going_away

Sent immediately before the server initiates a graceful shutdown. Followed by a normal WebSocket close with code 1001.

{ "type": "going_away", "retryAfterMs": 1000 }

Reconnect after the suggested interval. Don't treat this as a hard error.

JSON encoding rules

  • All messages are JSON, UTF-8, no leading whitespace, no comments.
  • Numbers are JSON numbers (not strings).
  • Maximum inbound frame size: 64 KB. Larger frames are closed with code 1009.
  • Maximum requestId length: 128 bytes.
  • Maximum peerId length: 128 bytes, printable ASCII only.

What's next