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'schannelPatterns.includeSenderMetadata— optional, defaultfalse. Whentrue, everymessageyou receive on this channel carries the sender'speerMetadataunderfromMetadata. 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 thesubclaim; 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.nullfor 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'smetadataclaim. Conventionally used to shipiceServersfor 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'speerId.fromMetadata— present only if you subscribed withincludeSenderMetadata: trueAND the publisher's JWT carriedpeerMetadata. 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 haspeerIdplus optionalmetadata(the peer'speerMetadataJWT claim).- A subscribe also produces a one-time presence event listing everyone already in the room before broadcasting your own
joinedto 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
requestIdlength: 128 bytes. - Maximum
peerIdlength: 128 bytes, printable ASCII only.
What's next
- Authentication — JWT claims, pk/sk key model
- Channels — wildcard patterns, reserved namespaces
- Presence & Metadata —
peerMetadata, per-message stamping - Close Codes and Error Codes