Skip to main content

Live PvP API

Live battles use authenticated HTTP reads for recovery and a WebSocket connection for actions and updates. The server owns match state and deadlines; a connected client displays the returned view and submits intent.

Ranked matchmaking supplies a match ID after both players accept a pairing. This page covers the battle connection. See Player battles for the journey and Battle rules for combat, turn order, and timeout behaviour.

HTTP access​

Both routes require bearer authentication and an active player account.

MethodRouteResult
GET/api/v1/matches/live/:matchId200 with the caller's current participant or spectator view.
POST/api/v1/matches/live/:matchId/connection-tickets201 with a single-use connection ticket.

GET uses the general quota; ticket creation has its own request quota. See the API reference for shared authentication and HTTP errors.

Use the current HTTP capability to include voluntary Creature swaps:

Accept: application/vnd.wits-world.live.v2+json, application/json;q=0.9

Without it, projections omit swap legal actions and redact swap details from history. The WebSocket capability is negotiated separately below.

Open a connection​

Request a ticket with { "role": "participant" } or { "role": "spectator" }. The server verifies the requested role against the caller's access. The response contains ticket, role, protocolVersion: 1, expiresAt, webSocketUrl, and requestId.

Tickets expire after 60 seconds and can be used once. Open the returned URL with the ticket as its ticket query parameter. Add battleSwaps=1 to receive the current swap-capable projection:

wss://<api-host>/api/v1/realtime/live?ticket=<ticket>&battleSwaps=1

Send the bearer token only to the HTTP endpoint. Keep tickets out of logs and obtain a fresh ticket for every reconnect; an expired or already redeemed ticket cannot open another connection.

After redemption, connection.ready supplies the role and complete current match. A new participant connection supersedes that player's previous connection to the same match. The old connection receives LIVE_CONNECTION_SUPERSEDED and closes with code 4409; it cannot continue submitting actions.

Participant and spectator views​

HTTP and state-bearing socket messages use the same role-specific match shape. Both include match identity, mode: "live", role, status, version, phase, turnNumber, turnDeadline, consecutiveTimeouts, public history, and lifecycle timestamps. HTTP adds requestId and serverTime; socket messages include their own serverTime.

ViewAdditional fields and visibility
ParticipantselfSide, self, opponent, legalActions, lastAction, outcome, and completionReason. Your cards are visible; unrevealed opponent cards are hidden. currentActor uses self or opponent.
SpectatorplayerOne, playerTwo, public effects, winner, and presence. Only revealed cards and remaining counts are visible. currentActor uses playerOne or playerTwo. No legal actions are exposed.

selfSide maps the participant to the stable playerOne or playerTwo history side. During initial selection, the opponent's selected Creature stays hidden until both opening selections resolve.

Spectator presence includes participant connection booleans and an aggregate spectator count. It does not identify spectators or expose private account, deck, or location data. Participants can fetch their completed match; new spectator reads and tickets are limited to active matches. Missing or unavailable matches return 404 LIVE_MATCH_NOT_FOUND.

Client messages​

Send a UTF-8 JSON text object with protocolVersion: 1, matchId, a UUID messageId, and a non-negative integer expectedVersion. Extra fields, binary messages, unknown types, and messages over 16,384 bytes are rejected.

An action uses type: "action.submit":

{
"protocolVersion": 1,
"type": "action.submit",
"matchId": "11111111-1111-4111-8111-111111111111",
"messageId": "22222222-2222-4222-8222-222222222222",
"expectedVersion": 7,
"action": {
"type": "use_move",
"moveId": "33333333-3333-4333-8333-333333333333"
}
}

The supported action types and identifier fields are:

Action typeIdentifier field
select_initial_creaturecreatureId
select_replacementcreatureId
swap_creaturecreatureId
use_movemoveId
use_power_cardpowerCardId

Use the latest legalActions and their returned IDs. The server revalidates each action against the current match version and deadline. Clients do not submit damage, effects, random outcomes, or timeout actions.

Resignation uses type: "resign.submit" with the same envelope fields and no action. It is available to an active participant regardless of whose turn it is. Spectators cannot submit either message type.

Server messages​

Every message identifies protocolVersion: 1. State-bearing messages contain the committed version and serverTime.

TypePurpose and key fields
connection.readyConfirms connection with connectionId, role, version, and complete match.
match.snapshotReplaces local state with eventId, version, and complete match.
match.transitionPublishes eventId, previousVersion, version, and complete match.
action.acknowledgedConfirms a committed or replayed command using messageId, eventId, and version.
presence.changedReports participant connection booleans and spectator count; it does not advance gameplay.
errorReports code, safe detail, retryable, and an optional messageId.

Acknowledge and transition delivery can occur separately. Use messageId to resolve pending commands, eventId to recognise repeated events, and version to order gameplay. Replace state from complete projections rather than reconstructing combat from messages. Ignore older versions; a version gap or ambiguous outcome can be repaired with an HTTP GET.

Deadlines and reconnect​

Each required decision has a 60-second deadline. Use serverTime and turnDeadline for the displayed countdown. The server checks database time when the action obtains authority; arrival at the gateway before the deadline does not guarantee acceptance. At the deadline, the action is late.

A missed decision advances the server's timeout handling. After 3 consecutive missed decisions, the participant loses by abandonment_forfeit. Reconnecting, reading state, and transport ping/pong do not pause or reset deadlines. The decision rules explain automatic selection, replacement, and normal-turn timeouts.

Reconnect with a fresh ticket and replace local state from connection.ready. Do not automatically replay an old action. For an intentionally retried command, reuse the same messageId and identical payload. An identical retry acknowledges the committed action without another transition; changed intent returns LIVE_IDEMPOTENCY_CONFLICT.

Existing sockets receive the final committed view and close normally with code 1000. Participants can then recover the result through HTTP.

Worker coordination safety​

In Upstash scheduler mode, each API instance reads one combined live/ranked activity snapshot with Redis HMGET every 10 seconds. This remains below the 30-second activity lease and 20-second proposal window; PostgreSQL match state, deadlines, locks, and versions remain authoritative.

SDK automatic deserialization is disabled, so the reader maps the five ordered HMGET values explicitly. A completely missing state initializes through EVAL with both workers active. Partial or malformed state fails safely and retains fast scans instead of declaring the workers idle.

Coordination diagnostics expose only a fixed activity_error_category (transport, timeout, invalid_response, or unknown), the coordination_stage, and, for idle failures, the fixed worker category. Raw provider errors, state values, keys, credentials, and URLs are never logged.

Errors and recovery​

CodeCaller action
LIVE_VERSION_CONFLICTReconcile the supplied snapshot or fetch current state before deciding again.
LIVE_ACTION_LATEShow the current snapshot and resolved timeout; do not resubmit the late intent as a new action.
LIVE_ACTION_ILLEGALUse current legal actions after refreshing state.
LIVE_MATCH_COMPLETEDRetrieve the final participant result.
LIVE_CONNECTION_SUPERSEDEDStop using the old socket; continue with the newer connection.
LIVE_TICKET_EXPIRED or LIVE_TICKET_ALREADY_USEDObtain a fresh ticket before connecting.
LIVE_ACTION_FORBIDDENCheck the connection role and match; spectators have read access only.

Socket errors use the message format above, while HTTP errors use the shared API format. retryable does not authorise replaying changed intent. For a lost acknowledgement, reconcile current state before intentionally retrying the original command. Ranked rating and territory results are read through the ranked settlement endpoint.

AI declaration​

Rewritten with assistance from Codex[GPT-6], using live routes, message handling, and role-specific projections. Earlier versions also used Codex[GPT-5] and Codex-CLI[GPT-5].