Guides

Tournaments

AdminUpdated Sep 22, 2026

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

scheduled

Created with a future startsAt. Players can join, but scores don't count yet.

live

Inside the window (startsAt ≤ now < endsAt). Accepted scores count.

ended

Brief internal state while prizes are being paid. It can't be canceled.

settled

Final. Ranks and prizes are stamped and the prize pool has been distributed.

canceled

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 ───────────┴──► canceled

Transitions happen automatically

The platform checks tournaments in the background about every 30 seconds:

  • A scheduled tournament becomes live once startsAt has passed.

  • A tournament whose endsAt has passed is settled: prizes are paid, standings are frozen, and your tournament.ended webhook 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 tournaments scope (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 with prizePoolCoins above 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 gets 403 session_required for 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

gameId or gameSlug

string

One of them

The game to run it on

name

string

Yes

1–120 characters

description

string

No

Up to 1,000 characters

metric

"high_score" or "total_score"

No

Default "high_score". See Scoring.

prizePoolCoins

integer

No

0 to 1,000,000. Default 0, for no prizes.

maxWinners

integer

No

1–50. Default 3.

startsAt

ISO 8601 datetime (UTC, e.g. 2026-10-01T18:00:00Z)

No

Default: now. A future time creates a scheduled tournament.

endsAt

ISO 8601 datetime (UTC)

Yes

Must be after startsAt

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

POST /v1/dev/tournaments

Create (limit: 20 per hour per creator)

GET /v1/dev/tournaments?gameSlug=… (or ?gameId=…)

Your tournaments in every status, newest first, up to 100, each with an entries count

POST /v1/dev/tournaments/:id/cancel

Cancel and refund the pool. Returns { ok: true, tournament }.

GET /v1/tournaments/:id

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

no_game

400

Neither gameId nor gameSlug was given

not_found

404

No such game

forbidden

403

Not your game, or the key lacks the tournaments scope

session_required

403

prizePoolCoins above 0 was sent with an API key. Create prize tournaments while signed in.

not_published

400

The game isn't published

bad_window

400

endsAt isn't after startsAt

insufficient_coins

400

Your coin balance can't cover prizePoolCoins

validation_error

400

A field is out of range or malformed

rate_limited

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() or GameSDK.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

high_score

Their single highest accepted score during the window

Classic "beat the high score"

total_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.

  1. Higher tournament score first.

  2. On equal scores, whoever reached that score first ranks higher. For high_score that's the time of the player's best score. For total_score it's the time of their latest accepted score.

  3. 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: 0 the 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

already_final

It's already settled or canceled

cannot_cancel

It's mid-settlement (ended), or its status changed concurrently

forbidden

Not your tournament

not_found

No such tournament

What players see

  • In your game: whatever you build with GameSDK.tournaments. A HUD banner from getActive() 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, name and gameId

  • winners: a list of { userId, rank, prizeCoins } for every player whose rank is maxWinners or better. Tied players are all included, so the list can be longer than maxWinners. 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 (maxWinners)

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

Was this page helpful?