Tournaments
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Tournaments
A tournament is a time-boxed score competition on your game, with an optional coin prize pool that you fund.
You create and manage tournaments from your server, with the REST API or the Publisher SDK, or from your game's dashboard page.
Players take part just by playing. Every accepted score submission automatically counts toward any tournament on that game whose window is open.
When the window closes, the platform ranks everyone who scored, pays the prize pool to the top finishers, and freezes the standings.
Prizes are coins, the platform's virtual currency. They have no real-money value and can't be cashed out. Tournaments involve no real-money payouts, entry fees or wagering. Players enter for free.
For exact SDK signatures, see GameSDK reference: ads, economy & tournaments.
Lifecycle
Status | Meaning |
|---|---|
| Created with a future |
| Inside the window ( |
| Brief internal state while prizes are being paid. It can't be canceled. |
| Final. Ranks and prizes are stamped and the prize pool has been distributed. |
| Canceled by the creator before settlement. The prize pool was refunded and no prizes were paid. |
scheduled ──(startsAt reached)──► live ──(endsAt reached)──► ended ──► settled
│ │
└──────────── cancel ───────────┴──► canceledTransitions happen automatically
The platform checks tournaments in the background about every 30 seconds:
A
scheduledtournament becomesliveoncestartsAthas passed.A tournament whose
endsAthas passed is settled: prizes are paid, standings are frozen, and yourtournament.endedwebhook is sent.
So settlement normally happens within about 30 seconds of endsAt, whether or not anyone is looking. Reading a tournament (for example GameSDK.tournaments.list(), getStandings(), the tournament's page, or your own GET /v1/dev/tournaments) applies a due transition straight away too.
Scores don't wait for the status to change. A score counts if it's submitted inside the window (startsAt ≤ now < endsAt), even in the few seconds before the status shows live. See Scoring.
Creating a tournament
Requirements
Your API key has the
tournamentsscope (see API keys & scopes), or you're signed in to the dashboard. - You own the game, and the game is published. - A prize pool needs a signed-in session. Funding a pool spends your coins, so a tournament withprizePoolCoinsabove 0 can only be created while you're signed in, for example from your game's dashboard page. An API key (and so the Publisher SDK) can create free tournaments (prizePoolCoins: 0), and gets403 session_requiredfor anything else.If you set a prize pool, your coin balance covers it. The whole pool is debited from your balance and held in escrow in the same step that creates the tournament: if creation fails, nothing is debited.
Fields
Field | Type | Required | Rules and default |
|---|---|---|---|
| string | One of them | The game to run it on |
| string | Yes | 1–120 characters |
| string | No | Up to 1,000 characters |
|
| No | Default |
| integer | No | 0 to 1,000,000. Default 0, for no prizes. |
| integer | No | 1–50. Default 3. |
| ISO 8601 datetime (UTC, e.g. | No | Default: now. A future time creates a |
| ISO 8601 datetime (UTC) | Yes | Must be after |
Over REST, send datetimes in UTC with a Z suffix. A timezone offset such as +02:00 is rejected with validation_error. The Publisher SDK converts a Date or date string to this form for you.
REST
curl -X POST https://api.coolgptgames.com/v1/dev/tournaments \
-H "authorization: Bearer $API_KEY" \
-H "content-type: application/json" \
-d '{
"gameSlug": "my-game",
"name": "Weekend Sprint",
"metric": "high_score",
"maxWinners": 3,
"endsAt": "2026-10-05T23:00:00Z"
}'This creates a free tournament (a pure ranking). To add a coin prize pool, create the tournament from your game's dashboard page while signed in.
Response (201):
{
"tournament": {
"id": "…",
"name": "Weekend Sprint",
"description": null,
"metric": "high_score",
"status": "live",
"prizePoolCoins": 0,
"maxWinners": 3,
"startsAt": "2026-10-03T12:00:00.000Z",
"endsAt": "2026-10-05T23:00:00.000Z",
"game": { "slug": "my-game", "title": "My Game" }
}
}Endpoint | Purpose |
|---|---|
| Create (limit: 20 per hour per creator) |
| Your tournaments in every status, newest first, up to 100, each with an |
| Cancel and refund the pool. Returns |
| Public detail and standings (see below) |
Publisher SDK
const t = await client.createTournament({
gameSlug: "my-game",
name: "Weekend Sprint",
metric: "high_score",
maxWinners: 3, // free: API keys can't fund a prize pool
endsAt: new Date(Date.now() + 2 * 24 * 3600 * 1000), // Date or ISO string
});
const mine = await client.listTournaments({ gameSlug: "my-game" });
const { tournament, standings } = await client.getTournament(t.id);
await client.cancelTournament(t.id);The Publisher SDK converts Date values to ISO strings for you. See Publisher SDK for client setup.
Creation errors
Code | HTTP | Cause |
|---|---|---|
| 400 | Neither |
| 404 | No such game |
| 403 | Not your game, or the key lacks the |
| 403 |
|
| 400 | The game isn't published |
| 400 |
|
| 400 | Your coin balance can't cover |
| 400 | A field is out of range or malformed |
| 429 | More than 20 creations in an hour |
Scoring
Tournaments are fed by the scores your game already submits with GameSDK.submitScore(), or by GameSDK.replay.submit() if you use verified replays. You don't submit tournament scores separately. See Scores, leaderboards & anti-cheat.
A submission counts toward a tournament only if all of these are true:
The player is signed in. Guest scores never enter tournaments.
The play session is active. Your game called
GameSDK.start()orGameSDK.replay.ready(), and the session hasn't ended.The session has enough genuine play: about one minute of real playtime (at least 4 heartbeats) before the score is sent.
The score is a finite, non-negative number. It's rounded down to an integer.
The score passes the platform's plausibility checks. A score held for review doesn't count toward tournaments, even though the call itself looks successful.
The current time is inside the tournament's window (
startsAt≤ now <endsAt), and the tournament hasn't been canceled or settled.The player is under the score rate limit (30 submissions per minute per game).
Each accepted submission goes to every tournament on the game whose window is open.
Metric | A player's tournament score is | Best for |
|---|---|---|
| Their single highest accepted score during the window | Classic "beat the high score" |
| The sum of all their accepted scores during the window | Grind or endurance events |
With total_score, every accepted submitScore call adds to the total. Submit once per completed run. Don't submit on every checkpoint, or you'll inflate totals.
Tournament scores use the same plausibility gate as the leaderboard, but they don't wait for replay verification. A score that is accepted but later fails replay verification still counts toward the tournament.
Ranking and ties
The same ranking is used everywhere: live standings, a player's own myRank, and settlement.
Higher tournament score first.
On equal scores, whoever reached that score first ranks higher. For
high_scorethat's the time of the player's best score. Fortotal_scoreit's the time of their latest accepted score.Players with the same score reached at exactly the same moment are an exact tie. They share a rank, and the next rank is skipped (competition ranking: 1, 2, 2, 4).
Exact ties are rare, because scores are timestamped when they're accepted. Players who joined but never scored have no rank (null) and are listed after everyone who scored.
Joining
Players don't have to join. Their first accepted score enrols them automatically. Joining is optional and lets a player appear on the board before they've scored, with a score of 0 and no rank:
in-game:
GameSDK.tournaments.join(id)on the tournament's page on the site: Join tournament
Joining is allowed while the tournament is scheduled or live and before endsAt. It's free, and joining twice does nothing extra. A joined player's first accepted score becomes their tournament score as-is (and ranks them), whatever the metric.
Prizes
At settlement the prize pool is split across N paid places, where N is maxWinners or the number of players who scored, whichever is smaller. The places get a descending linear weighting: place 1 gets weight N, place 2 gets N−1, and so on down to 1. Each share is rounded down, and any rounding remainder goes to place 1.
Pool | Paid places | Shares |
|---|---|---|
1,000 | 3 | 501 · 333 · 166 |
5,000 | 5 | 1,668 · 1,333 · 1,000 · 666 · 333 |
10 | 3 | 6 · 3 · 1 |
100 | 1 | 100 |
Ties
Players in an exact tie (see Ranking and ties) occupy consecutive places, and they split the shares of those places equally, rounded down. Only paid places count: if a tie runs past the last paid place, the tied players split just the paid shares they cover.
Worked example: a pool of 1,000 with maxWinners: 3, so the shares are 501 · 333 · 166.
Player | Score | Rank | Places occupied | Prize |
|---|---|---|---|---|
Ana | 900 | 1 | 1 | 501 |
Bo | 700 (exact tie) | 2 | 2 and 3 | ⌊(333 + 166) ÷ 2⌋ = 249 |
Cy | 700 (exact tie) | 2 | 2 and 3 | 249 |
Di | 500 | 4 | 4 (unpaid) | 0 |
That pays out 999 coins. The 1 coin lost to rounding the tie split returns to your balance.
Things to know:
The pool is always fully accounted for. If fewer players scored than
maxWinners, fewer places are paid, and the pool is split over the places that exist. Any coins not paid out, including remainders from splitting ties, return to your balance.Only players who scored can win. A player who joined but never submitted an accepted score gets no rank and no prize, however small the tournament.
Winners get their coins added to their balance and a notification ("You placed #N in … and won … coins!").
With
prizePoolCoins: 0the tournament runs as a pure ranking. No coins move.
Canceling
POST /v1/dev/tournaments/:id/cancel (or client.cancelTournament(id)) works only while the tournament is scheduled or live.
The full prize pool is refunded to your coin balance.
No prizes are paid.
The tournament disappears from your game's in-game list.
Code | Cause |
|---|---|
| It's already |
| It's mid-settlement ( |
| Not your tournament |
| No such tournament |
What players see
In your game: whatever you build with
GameSDK.tournaments. A HUD banner fromgetActive()works well:GameSDK.tournaments.getActive().then(function (t) { if (!t) return hideBanner(); showBanner(t.name + ": " + t.prizePoolCoins + " coin pool" + (t.myRank ? ", you're #" + t.myRank : "")); // myRank is null until the player scores });On the site: a tournaments hub lists live and upcoming tournaments across all games, sorted by prize pool. Each tournament also has its own page showing:
its status, metric ("High score" or "Total score"), prize pool, number of paid places and player count
a Join tournament button
standings, with each winner's prize after settlement
Notifications: winners are notified when prizes are paid.
Webhook
When a tournament settles, the platform sends your tournament.ended webhook. Its data includes:
tournamentId,nameandgameIdwinners: a list of{ userId, rank, prizeCoins }for every player whose rank ismaxWinnersor better. Tied players are all included, so the list can be longer thanmaxWinners. Players who never scored are never listed.
The webhook is sent when settlement runs, normally within about 30 seconds of endsAt. No webhook is sent for cancellations. See Webhooks for signing and delivery.
Limits
Limit | Value |
|---|---|
Creations | 20 per hour per creator |
Prize pool | 0 – 1,000,000 coins |
Paid places ( | 1 – 50 |
Standings returned | Top 100 |
In-game list | Up to 20 per game. Canceled tournaments are excluded, and settled ones drop off 7 days after settlement. |
Score submissions | 30 per minute per player per game |
Related
Scores, leaderboards & anti-cheat: how scores are accepted - In-game economy: coins - Webhooks, Publisher SDK, REST API overview, API keys & scopes, Errors & limits