Skip to content

API Reference

Two communication layers: REST for data reads/writes, Socket.IO for real-time game events. All authenticated endpoints require a Clerk JWT in Authorization: Bearer <token>.


REST Endpoints

Matches — GET /matches

Returns the authenticated user's matches, newest first.

Query paramTypeDefault
limitnumber ≥ 1
offsetnumber ≥ 0

Response 200:

json
{
  "matches": [
    {
      "id": "match_1234567890_abc123def",
      "status": "active",
      "gameMode": "default",
      "currentTurn": "<playerId>",
      "winner": null,
      "createdAt": "2026-05-01T12:00:00.000Z",
      "completedAt": null,
      "players": [{ "id": "...", "name": "...", "socketId": "..." }],
      "moves": [
        {
          "playerId": "...",
          "moveType": "playBand",
          "data": {},
          "timestamp": "..."
        }
      ],
      "currentTimer": {
        "playerId": "...",
        "turnStartedAt": "...",
        "maxTurnTime": 20000
      },
      "maxTurnTimeMs": 20000,
      "lastBandName": "Radiohead"
    }
  ]
}

Player names are enriched from Clerk display names. Bot player uses "Roboto".

Matches — GET /matches/:id

Single match by ID. Returns 404 if not found or user is not a participant.

Matches — POST /matches/singleplayer

Create a human-vs-bot match.

Body: { playerId?: string; playerName?: string }
Response 201: Created match object.

Matches — POST /matches/rematch

Create a rematch from a finished match.

Body: { matchId: string }
Response 201: New match object.
Response 404: Match not found or not yet completed/abandoned.

Statistics — GET /statistics

Aggregated win/loss stats for the authenticated user. See apps/server/docs/statistics-api.md.

Statistics — GET /statistics/time-series

Time-bucketed stats for charting. See apps/server/docs/statistics-api.md.

XP — GET /xp/me

Current XP, level, rank, and progress to next level. See XP & Ranks.

Config — GET /config

Public app config (live store items, menu buttons, match config). No auth required.

Config — Admin endpoints

GET /config/full, PUT /config, POST /config/reload, GET /config/rewards, PUT /config/rewards — all require x-admin-key header.

See Social API for friends, users, challenges, and push notification documentation.


Socket.IO — Game Gateway

Handles all in-match events. Players join a room named after gameId.

Game Gateway — Client → Server

EventPayloadNotes
joinGame{ gameId, playerId }Join match room, receive current state
makeMove{ gameId, playerId, moveType: 'playBand', moveData: { bandName } }Submit a band name
revealBand{ gameId, bandId, playerId }Reveal opponent's hidden band; starts turn timer
abandonGame{ gameId, playerId }Forfeit the match immediately
clientTimeout{ gameId, playerId }Notify server timer expired client-side
getGameState{ gameId, playerId }Request full game state sync
getGameBands{ gameId, playerId }Request band list (filtered per player perspective)
playerTyping{ gameId, playerId, isTyping }Typing indicator (ephemeral)

Game Gateway — Server → Client

EventPayloadWhen
gameStateFull GameState objectAfter any state change
gameBandsFiltered band listAfter getGameBands or state change
moveCompleted{ playerId, moveType, moveData, timestamp }Valid move submitted
moveError{ error, illegal?, reason?, gameState? }Move rejected or illegal
timerStarted{ playerId, turnStartedAt, maxTurnTime }Band revealed, timer begins
timerCleared{ playerId }Valid move cleared the timer
turnTimeout{ playerId }Player timed out
timeoutError{ error }clientTimeout rejected (grace period not met)
gameAbandoned{ playerId }Player abandoned
abandonError{ error }Abandon failed
revealError{ error }Reveal failed
opponentTyping{ gameId, playerId, isTyping }Opponent is typing
playerJoined{ playerId }Player joined the room
gameError{ error }Game not found or other error
matchFound{ matchId }Match ready (also emitted here after join)
autoJoinedGame{ matchId, playerId }Confirmation of room join
matchReady{ matchId, players }All players joined, game can start

Socket.IO — Matchmaking Gateway

Handles identity, queue, challenge, and rematch flows.

Matchmaking Gateway — Client → Server

EventPayloadNotes
identify{ playerId, name, expoPushToken? }Must call on connect/reconnect
appStateChange{ state }'active' = foreground; used for push notification suppression
joinQueuePlayer objectEnter matchmaking queue for gameMode
leaveQueue{ playerId }Exit queue
createSingleplayerMatch{ playerId?, playerName? }Create human-vs-bot match via socket
sendChallenge{ recipientId }Challenge a friend (no first move)
sendChallengeWithFirstMove{ recipientId, bandName }Challenge + play first band immediately
acceptChallenge{ challengeId }Accept incoming challenge
declineChallenge{ challengeId }Decline incoming challenge
cancelChallenge{ challengeId }Challenger cancels before acceptance
requestRematch{ matchId, playerId }Request a rematch

Matchmaking Gateway — Server → Client

EventPayloadWhen
identifiedPresence registered
queued{ status: 'waiting', queueStatus: { playersInQueue, estimatedWaitTime } }Entered queue, waiting
queueLeftSuccessfully left queue
queueLeaveErrorLeave failed
singleplayerMatchCreated{ matchId, players, match }Singleplayer match ready
challengeSent{ challengeId, recipientId, matchId? }Challenge dispatched to recipient
challengeReceived{ challengeId, challengerId, challengerName, matchId? }Incoming challenge
challengeAccepted{ challengeId, matchId }Your challenge was accepted
challengeDeclined{ challengeId }You declined a challenge
challengeDeclinedByRecipient{ challengeId }Recipient declined your challenge
challengeCancelledByChallenger{ challengeId }Challenger cancelled before you accepted
rematchCreated{ match }Rematch match object
matchFound{ matchId }Queue matched, game starting