Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Leaderboards and tournaments

This page describes leaderboards, where players compete on score, and elimination-style tournaments. Read it if you want to write scores from a trusted server and show the ranking in your game.

What it does

The competition module (competition) provides two things:

  • Leaderboard: A ranking that keeps each player’s best score and can be split into seasons.
  • Tournament: A single-elimination, double-elimination or round-robin bracket with player or clan participants, with a final prize.

Writing scores and managing boards is done with a server key. The game client reads the board with a player session and does not need a server key.

If the module is disabled, the endpoints return 404 with {"code":"module_disabled","module":"competition"}. Enable the module on the Game settings › Modules page.

Where it lives in the panel

What you want to doPanel page
Define a board; set season, unit and visibilityLeaderboard
See a board’s top entries and season historyLeaderboard detail
The prize currency for a tournamentCurrencies

There is no panel screen for tournaments; they are managed only with REST.

Note

Panel help pages are not translated yet; the panel links above point to the Turkish source.

Data model (in plain words)

  • Board definition: key, name, metric_expr, order (asc: a smaller score is better, desc: a larger score is better), season, tie_policy, player_visible, unit and unit_suffix. A definition is versioned.
  • Season (season): none (never reset), daily, weekly, monthly (aligned to the UTC calendar: a day turns over at 00:00 UTC, a week on Monday at 00:00 UTC, a month on the first of the month) or custom (a period of custom_period_ms length). When a season closes, its ranking is stored as an immutable record.
  • Score: An integer (i64). A player’s best score in the season counts (the highest for desc, the lowest for asc). Sending a worse score does not change the ranking.
  • Tie-break (tie_policy): For now the only option is score_achieved_player_id: score first, then whoever reached that score first, then the player ID.
  • Rank (rank): Starts at 1.
  • Unit (unit): points, money (smallest currency unit) or duration_ms; unit_suffix is a 1–8 character suffix ($, ms …). The unit is for display only.
  • player_visible: If true, a player session can read the board; the default is false.
  • metric_expr: A CEL expression compiled at registration (required, cannot be empty). Boards that plugins create on their own use event.score; write the same expression when you create one with REST.

Calls your game makes

Method and pathCredentialWhat it does
POST /v1/leaderboardsserver keyCreates a board definition
GET /v1/leaderboards, GET /v1/leaderboards/{key}server keyLists / reads definitions
POST /v1/leaderboards/{key}/scoresserver keyWrites a player score (Idempotency-Key required, 202)
GET /v1/leaderboards/{key}/entries?limit=&cursor=server keyTop ranks (paginated)
GET /v1/leaderboards/{key}/around-me?player_id=&radius=server keyRanks around a player
GET /v1/player/leaderboards/{key}?limit=player sessionThe first page of the board and the player’s own row
POST /v1/tournamentsserver keyCreates a tournament
GET /v1/tournaments/{id}server keyReads the tournament state
POST /v1/tournaments/{id}/participantsserver keyRegisters a participant (Idempotency-Key)
POST /v1/tournaments/{id}/startserver keyGenerates the bracket and starts
POST /v1/tournaments/{id}/matches/{match_id}/resultserver keyReports a match result or resolves a dispute (Idempotency-Key)

Server endpoints require Authorization: Bearer crx_live_<prefix>_<secret>; a client key and a player session are rejected. The player endpoint works with a cookie or session token and x-crx-env: <environment uuid>; a developer key is not accepted.

Note

The TypeScript SDK has player.leaderboard(key, { limit }) for player reads; there is no typed client for the server endpoints (writing scores, definitions, tournaments), which are called with raw REST. See TypeScript SDK.

Board definition

POST /v1/leaderboards
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json

{
  "key": "daily-earnings", "name": "Daily earnings",
  "metric_expr": "event.score", "order": "desc",
  "season": "daily", "tie_policy": "score_achieved_player_id",
  "player_visible": true, "unit": "money", "unit_suffix": "$"
}

Returns 201 with { id, key, name, metric_expr, order, season, custom_period_ms, tie_policy, season_key }. For season: "custom", custom_period_ms must be positive.

Writing a score

POST /v1/leaderboards/daily-earnings/scores
Authorization: Bearer crx_live_<prefix>_<secret>
Idempotency-Key: score-player-1234-2026-11-03
Content-Type: application/json

{ "player_id": "0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f", "score": 4200 }
{ "entry": { "rank": 7, "player_id": "0192f0f2-…", "score": 4200, "achieved_at_ms": 1790000000000 }, "replayed": false }

The response is 202: the score has been durably accepted; the ranking projection is updated after a short delay, so a board read immediately afterwards can lag by a few seconds. Idempotency-Key is required: the same key and the same body return the same result with replayed: true.

Reading the board with a player session

GET /v1/player/leaderboards/daily-earnings?limit=10
x-crx-env: 0192f000-0000-7000-8000-00000000e001
{
  "leaderboard": { "key": "daily-earnings", "name": "Daily earnings", "order": "desc", "season": "daily", "seasonKey": "2026-11-03", "seasonEndsAtMs": 1790121600000, "unit": "money", "unitSuffix": "$" },
  "entries": [ { "rank": "1", "playerId": "…", "displayName": "Ayşe", "score": "9800", "achievedAtMs": 1790000000000 } ],
  "me": { "rank": "7", "playerId": "…", "displayName": "You", "score": "4200", "achievedAtMs": 1790000000000 }
}
  • Only player_visible boards can be read; otherwise 404. 404 also if the module is disabled.
  • limit is 1–50 (default 20).
  • rank and score are decimal strings in JSON (lossless i64); the SDK turns them into bigint. Field names on this endpoint are camelCase.
  • Banned and deleted players are filtered out and ranks are numbered densely within the page; me is your rank among visible players. If you have no score this season, me is null.
  • The first page is cached for a short time (at most a few seconds).

Tournaments

POST /v1/tournaments
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json

{
  "name": "Winter Cup", "format": "single_elimination", "participant_type": "player",
  "capacity": 16,
  "registration_opens_at_ms": 1790000000000, "registration_closes_at_ms": 1790086400000,
  "seeding_source": "registration_order",
  "prize_table": { "currency_key": "gold", "amount": 5000 }
}
  • format: single_elimination, double_elimination, round_robin. participant_type: player or clan.
  • capacity is 2–1024; the registration close must be after the open; name is 1–200 characters.
  • seeding_source: registration_order (registration order; leaderboard_id is not given) or leaderboard (player tournaments only; leaderboard_id is required).
  • prize_table: an object with currency_key (no whitespace, at most 200 characters) and a positive amount.
  • Flow: create → register with participants ({ "participant_id" }) → generate the bracket with start → report result for each match (reporter_id, reported_side: home away admin, outcome: home_win away_win draw, score). The admin side is for resolving a dispute.
  • Responses carry the tournament’s current view ({ "tournament": { … } }).

Versioning and replay safety

  • Writing a score, registering a participant and a match result require Idempotency-Key (the same key + the same body: does not run again; a different body: 422).
  • A board definition is versioned; list and ranking reads do not need a version header.
  • General rules: Idempotency and concurrency.

Events

Accepting a score produces the score_submitted event, and a season rollover produces season_closed/season_opened; these can be used in quest objectives and rule automation. Among the types you can select as a webhook filter there is no leaderboard or tournament event; endpoints registered without a filter receive all events, and leaderboard events are included, but the envelope format is not a documented contract. For the final prize, use the reward granting flow on the Rewards page.

Limits

  • limit is 1–50 on player reads; pagination on the server entries endpoints is cursor-based.
  • Tournament capacity is 2–1024.
  • season: custom needs a positive custom_period_ms.

Common errors

SymptomCauseFix
404 (player read)The board is not player_visible, does not exist, or the module is disabledCheck the definition
403A server endpoint called with a client key or a player sessionUse the server key from your server
400 (Idempotency-Key)The header is missing when writing a scoreSend a key
The score was written but the ranking did not changeThe player’s best score this season is betterOnly a better score changes the ranking
The score was written but the board did not update right awayProjection delayWait a few seconds and read again
400 (tournament)capacity, the registration window, prize_table or the seeding rule is wrongFix the body

Working example: write a score, read the board

/** Skor tablosu: sunucu puan yazar (ham REST), oyun istemcisi tabloyu oyuncu oturumuyla okur. */
import type { PlayerClient } from "@ferdiunal/open-play-ts";

/** Sunucu: `POST /v1/leaderboards/{key}/scores`. Sunucu anahtarı gerekir; Idempotency-Key zorunludur. */
export async function submitScore(baseUrl: string, serverApiKey: string, board: string, playerId: string, score: number, requestKey: string): Promise<number> {
  const response = await fetch(`${baseUrl}/v1/leaderboards/${encodeURIComponent(board)}/scores`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${serverApiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": requestKey,
    },
    body: JSON.stringify({ player_id: playerId, score }),
  });
  if (response.status !== 202) throw new Error(`Puan yazılamadı: HTTP ${response.status} ${await response.text()}`);
  const body = (await response.json()) as { entry: { rank: number }; replayed: boolean };
  return body.entry.rank;
}

/** Oyun istemcisi: tablonun ilk sayfası ve oyuncunun kendi satırı (`player_visible` tablolar). */
export async function showBoard(player: PlayerClient, board: string): Promise<void> {
  const view = await player.leaderboard(board, { limit: 10 });
  for (const row of view.entries) console.log(`${row.rank}. ${row.displayName} — ${row.score}`);
  if (view.me !== null) console.log(`Siz: ${view.me.rank}. sıra, ${view.me.score} puan`);
}