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 param | Type | Default |
|---|---|---|
limit | number ≥ 1 | — |
offset | number ≥ 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
| Event | Payload | Notes |
|---|---|---|
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
| Event | Payload | When |
|---|---|---|
gameState | Full GameState object | After any state change |
gameBands | Filtered band list | After 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
| Event | Payload | Notes |
|---|---|---|
identify | { playerId, name, expoPushToken? } | Must call on connect/reconnect |
appStateChange | { state } | 'active' = foreground; used for push notification suppression |
joinQueue | Player object | Enter 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
| Event | Payload | When |
|---|---|---|
identified | — | Presence registered |
queued | { status: 'waiting', queueStatus: { playersInQueue, estimatedWaitTime } } | Entered queue, waiting |
queueLeft | — | Successfully left queue |
queueLeaveError | — | Leave 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 |