WebSocket protocol
One WebSocket per client at /ws. All messages are JSON with a common envelope. Use this directly if you're writing a custom client; the SDKs wrap it.
Envelope
// Client → server
{"cid": "optional", "type": "message.type", "payload": {}}
// Server → client
{"cid": "echoed-if-request", "type": "message.type", "payload": {}}The cid is optional. When present, the server echoes it back so the client can correlate request/response pairs.
Session
session.connect (client)
First message; authenticates the connection.
{"type": "session.connect", "payload": {"token": ""}} session.connected (server)
Ack of session.connect.
{"type": "session.connected", "payload": {"player_id": "..."}}session.heartbeat (client)
Keep-alive ping; send periodically.
{"type": "session.heartbeat", "payload": {}}Matches
match.join (client)
Join a specific match (after matchmaking or invite).
{"type": "match.join", "payload": {"match_id": "..."}}match.input (client)
Send an input to the match.
{"type": "match.input", "payload": {"action": "move", "x": 10, "y": 5}}match.leave (client)
Leave the current match.
{"type": "match.leave", "payload": {}}match.started (server)
Match has begun.
{"type": "match.started", "payload": {"match_id": "...", "players": [...]}}match.state (server)
Broadcast state update (shape is game-specific, returned by your get_state callback).
{"type": "match.state", "payload": {"players": {...}, "tick": 42}}match.finished (server)
Match ended with a result.
{"type": "match.finished", "payload": {"match_id": "...", "result": {...}}}Matchmaking
matchmaker.add (client)
Submit a ticket.
{"type": "matchmaker.add",
"payload": {"mode": "arena", "properties": {"skill": 1200}}}matchmaker.remove (client)
Cancel a ticket.
{"type": "matchmaker.remove", "payload": {"ticket_id": "..."}}matchmaker.matched (server)
A match was found.
{"type": "matchmaker.matched", "payload": {"match_id": "...", "players": [...]}}Chat
chat.join / chat.leave (client)
Join/leave a channel.
{"type": "chat.join", "payload": {"channel_id": "lobby"}}
{"type": "chat.leave", "payload": {"channel_id": "lobby"}}chat.send (client)
Post a message.
{"type": "chat.send", "payload": {"channel_id": "lobby", "content": "Hello!"}}chat.message (server)
A new message in a joined channel.
{"type": "chat.message",
"payload": {
"channel_id": "lobby",
"sender_id": "...",
"content": "Hello!",
"sent_at": "2026-04-15T10:30:00Z"
}}Voting
vote.cast (client)
Cast a vote. For approval voting, option_id is a list.
{"type": "vote.cast",
"payload": {"vote_id": "...", "option_id": "jungle"}}
// approval voting
{"type": "vote.cast",
"payload": {"vote_id": "...", "option_id": ["jungle", "caves"]}}vote.veto (client)
Use a veto token to cancel. Requires veto_tokens_per_player > 0 and veto_enabled on the vote.
{"type": "vote.veto", "payload": {"vote_id": "..."}}match.vote_start (server)
A new vote has started.
{"type": "match.vote_start",
"payload": {
"vote_id": "...",
"options": [{"id": "jungle", "label": "Jungle Path"}, {"id": "volcano", "label": "Volcano Path"}],
"window_ms": 15000,
"method": "plurality"
}}match.vote_tally (server)
Running tally update (only with visibility: live).
{"type": "match.vote_tally",
"payload": {
"vote_id": "...",
"tallies": {"jungle": 2, "volcano": 1},
"time_remaining_ms": 8432,
"total_votes": 3
}}match.vote_result (server)
Vote closed, winner determined.
{"type": "match.vote_result",
"payload": {
"vote_id": "...",
"winner": "jungle",
"counts": {"jungle": 2, "volcano": 1},
"distribution": {"jungle": 0.666, "volcano": 0.333},
"total_votes": 3,
"turnout": 1.0
}}match.vote_vetoed (server)
A player vetoed the vote.
{"type": "match.vote_vetoed", "payload": {"vote_id": "...", "vetoed_by": "player_id"}}Presence & notifications
presence.update (client)
Update your online status.
{"type": "presence.update",
"payload": {"status": "in_game", "metadata": {"match_id": "..."}}}presence.changed (server)
A friend's presence changed.
{"type": "presence.changed", "payload": {"player_id": "...", "status": "online"}}notification.new (server)
A new notification for the player.
{"type": "notification.new",
"payload": {
"id": "...",
"type": "friend_request",
"subject": "New friend request",
"content": {"from_player_id": "..."}
}}Where next?
- REST API — HTTP endpoints for things that don't fit a real-time channel.
- Authentication — how to get the session token for
session.connect. - Voting in depth — methods, tie-breakers, weighted, ranked.