Tournaments
A tournament is a recurring rank table: players pay an entry fee, Challenge puts up a fixed purse announced before anyone enters, and the purse is paid by rank when the round closes.
Tournaments beside Challenges
A Challenge is a fixed standard. The player pays a fee and Challenge pays a fixed prize to anyone who beats the bar. A tournament is a fixed purse against a field. The player pays an entry fee and Challenge pays the announced amounts to whoever finishes on top.
The two share everything underneath. The same attempts, the same Challenge.startAttempt(), the same outcome endpoint, the same key, the same webhooks, the same island. A game that already offers Challenges offers tournaments without changing a line of code. You are not integrating twice.
| Challenge | Tournament | |
|---|---|---|
| What the player pays | A fee per attempt | An entry fee per attempt, flat for the first and every re-buy |
| Who funds the prize | Challenge | Challenge |
| When it is known | Before the player pays | Before the player pays |
| What decides who wins | A fixed bar | The rank table at the close |
| What Challenge earns | The fee | The entry fee |
The house is never on the other side of a score
Entry fees are Challenge's revenue, booked when an attempt is bought. They are not held, pooled, or earmarked for anyone's winnings. The purse is separate money Challenge commits before the round opens, out of its own funds.
Nothing about a score changes what Challenge earns or what the purse pays. There is no pool that grows with the field and no rake taken out of one player's stake to fund another's win. A round with thirty players and a round with three hundred pay exactly the same three amounts.
The one thing that can move is whether the round runs at all.
The purse, and the one condition
A tournament is one price. The entry fee is $20 per attempt, the purse is 50 times the fee, and the split is three ways at 51% / 31% / 18%.
| Entry fee | Purse | Scored attempts needed | Players needed | Paid |
|---|---|---|---|---|
| $20.00 | $1,000.00 | 70 | 12 | $510.00 / $310.00 / $180.00 |
The fee is per attempt, not a buy-in, so a player who uses all ten attempts spends $200 in a round.
Three positions and three dollar amounts, decided at spawn and true at any field size. A fixed purse that scaled its positions with turnout would make each winner's amount depend on how many people paid, which is the contingency the whole model exists to avoid. The card a player reads before paying shows the numbers settlement will pay.
Rank one is $510, and no top position may sit above your integrity tier ceiling. That is why tournaments need a tier B or A program: tier C's ceiling is $100, so no round at this purse fits under it, and a tier C program is refused a tournament rather than offered a smaller one.
The two conditions a round has to meet
A round runs only if it collected at least 70 scored attempts AND at least 12 distinct players posted a score. Short of either, it is cancelled and every entry fee is refunded in full.
The two answer different questions and neither substitutes for the other:
- 70 scored attempts is what pays for the purse. Fifty attempts at $20 is exactly the $1,000 purse, so 70 is break-even plus a 40% margin. It is a count of attempts rather than of players because attempts are what carry a fee: ten players buying seven attempts each clear it, thirty players buying one each do not.
- 12 players is the anti-farm floor. A rank table small enough for one group to occupy all three paid positions is not a contest, whatever it collected.
Both are frozen onto the round when it opens, so a change to either can never reach a round somebody has already paid into. Nobody plays for a purse that was never going to be paid, and nobody is out of pocket for a round that did not happen.
The deal in one line: the purse never changes, but the round only runs if enough attempts are played in it.
Creating one
Developer Portal, on the program. You supply a name and description, the metric name and unit, the direction, a tiebreak metric, an entry fee, and the UTC hour a new round opens.
The tiebreak metric, and why it is required
Two players will post the same score sooner than you expect. On a metric with a best possible value, fewest moves, a perfect accuracy, a completed puzzle, the top of the board ties constantly, because every strong player reaches the same optimum. A round ranked on one number has to break those ties somehow, and every somehow except a second measurement is hostile to somebody: first-to-post hands the rank to whoever got there first and tells everyone after them that matching it loses, which empties the round exactly when it needs entrants.
So a tournament declares a tiebreak metric at creation: a second number your game reports with every score, that orders players who tie on the first. Time is the usual choice for a score or move-count game (the fastest perfect game wins), moves or accuracy for a time game. It cannot be the same measurement as the metric itself, and creation is refused without it.
Your game then reports both numbers on every tournament attempt: { "score": 7, "tiebreak": 12.431 }. A report without the tiebreak is refused with tiebreak_required and the attempt stays open, so a half-wired integration fails loudly at the first report rather than quietly building an unrankable board. Ranking is score first, tiebreak second, and only then the clock, as pure determinism for the near-impossible identical pair.
The entry fee must come from the allowlist your integrity tier permits:
| Tier | Entry fees |
|---|---|
| C | none: tier C cannot run tournaments |
| B | $20 |
| A | $20 |
The tier is how well Challenge can verify what you report, and it reaches money through the ceiling on a single payout. A $1,000 purse pays $510 at rank one, which fits under tier B's ceiling and does not fit under tier C's, so a tier C program is refused with the tier requirement named rather than offered a fee that cannot work. A fee outside the list is refused with the list named. See Programs and keys.
Everything else is platform-owned and identical for every partner:
| Setting | Value |
|---|---|
| Prize funding | Challenge, from its own funds |
| Round duration | 48 hours |
| Rounds per tournament | Back to back, about three and a half a week |
| Attempt cap | 10 paid attempts per player per round |
| Entry cutoff | 10 minutes before the round closes |
| Reporting grace | 15 minutes after the round closes |
| Minimum scored attempts | 70, break-even fill plus a 40% margin, frozen at spawn |
| Minimum entrants | 12, the anti-farm floor, frozen at spawn |
| Purse split | Top three, at 51% / 31% / 18% |
There is no calibration step. A tournament prices nothing and has no bar to measure, so there is no sample to collect first: the first round opens on the next window, and every later round opens as the one before it ends.
The schedule is a grid, in UTC
Rounds tile a fixed lattice of instants: the spawn hour you chose, plus whole multiples of the round duration. Every round opens on a grid point and closes on the next one, so one round's close is the next round's open and the schedule a player learns in the first week is the schedule they keep. Nothing is measured from when the previous round happened to settle, so a settlement that runs late, or a day late, moves nothing.
The grid is UTC, and it is the whole reason the spawn hour is stated in UTC. No clock change anywhere moves a grid point, so a round is never an hour early, an hour long or an hour short twice a year.
A round is only ever opened into the front half of its own window. A tournament created partway through a window waits for the next grid point rather than opening a round that is already half over: that round would carry a few hours of entries against a full attempts gate, and would be cancelled at its midpoint anyway.
Editing or pausing a tournament never touches a round that is already running. Each round freezes its entry fee, purse, payout table, attempt cap, minimum scored attempts and minimum entrants when it opens and runs to completion on those. Changes apply from the next round, which is what lets a player treat the numbers on the card as a promise.
Pausing, resuming and closing
Three actions, on the tournament row in the Developer Portal.
| Action | What it does | What it does not do |
|---|---|---|
| Pause | No new round opens at the next grid point | Nothing to the open round: it keeps selling entries to its own cutoff and settles or cancels on schedule |
| Resume | Rounds open again from the next grid point | Nothing retroactive: the windows that passed while paused stay skipped |
| Close | Permanent. No new round opens, ever | Nothing to the open round, which finishes exactly as it would have |
None of the three is a stop button, and there is deliberately no stop button. A round holds money the field has already paid in, under terms it was sold on. Ending one early would either strand that money or refund a contest people are in the middle of winning, so the only two ways a round ends are the two it always had: it settles, or it fails its minimums and refunds everybody in full. Pausing at the worst possible moment costs a partner at most one more round.
Closing is final. There is no reopen, because a closed tournament has told its players it is over and putting a round back on a schedule they stopped watching is worse than making a new one. Creating another costs nothing: a tournament prices nothing and has no calibration step.
Pausing a live tournament pauses its sandbox twin with it, so the two never disagree about whether the tournament is running. The actions live on the Live view for that reason: the twin follows.
The round
opens_at ─────────────── entries_close_at ──── closes_at ──── settlement
| | | |
round opens, re-buys stop, scores stop standings,
attempts sell 10 min before being accepted payouts, next
the close round spawns| Moment | What is true |
|---|---|
opens_at | Attempts can be bought and scores can be reported |
entries_close_at | No further attempts can be bought, first entry or re-buy. Attempts already bought stay playable and reportable. |
closes_at | Scores are no longer accepted. Reporting one answers round_closed. |
closes_at + 15 min | Settlement runs |
The entry cutoff exists because standings are visible live. Without it the optimal play is to sit out, watch the board, and buy exactly enough attempts at the death to clear the last paid place. The reporting grace exists for the scores still travelling from your server when the bell goes.
Both are on the round object (entries_close_at), so read them rather than subtracting a constant yourself.
Entries and ranks
- One entry per player per round, created the first time they buy an attempt in it.
- Every attempt costs the same. No discount and no premium for a re-buy.
- At most 10 paid attempts per player per round. The cap is what keeps the table a skill contest rather than a spending contest.
- Only a player's best settled score counts. More attempts buy more chances at a better score, never more rows on the table.
- Ties break on the declared tiebreak metric: the second number your game reports with every score, so a late entrant who matches a score can still win it by outplaying it. The clock is last-resort determinism only, for the near-impossible identical pair.
- Only players with at least one settled score are ranked. An entrant who bought an attempt and never played is not on the table, does not count toward the minimum field, and their attempt is refunded rather than counted toward the minimum scored attempts.
Entering a round
From the island
The player arms a round, and your existing call enters it.
A tournament card in the island's expanded panel carries the same slide control a Challenge card does. Sliding it arms that round. Your game then starts a round exactly the way it already does:
const attempt = await Challenge.startAttempt()That call enters whatever the player armed, a Challenge or a tournament round. Everything after it is identical, attemptId included.
A player has at most one armed thing, of one kind. Arming a tournament replaces an armed Challenge and the other way round, which is what makes the fee they agreed to the fee they are charged.
Naming the round explicitly
If your own code decides which round a player is in:
const attempt = await Challenge.startAttempt({ tournamentRound: roundId })Nothing needs to be armed on this path, because naming the round is the decision. Both forms end in the same entry, with the same guards and the same failures.
From your server
Runs on your server. The same endpoint a Challenge attempt uses, with a different parent.
# The key lives on your server. It never reaches the browser.
curl -X POST https://api.withchallenge.com/v1/attempts \
-H "X-API-Key: $CHALLENGE_API_KEY" \
-H "Idempotency-Key: 5f1c9d4e-3b7a-4f61-8a9c-2d0e6b1f4c33" \
-H "Content-Type: application/json" \
-d '{
"tournament_round_id": "<<YOUR_ROUND_ID>>",
"player_ref": "player_42"
}'Send exactly one of challenge_id and tournament_round_id. Both, or neither, is invalid_request: the two are different deals, and picking one for you would be picking what your player's money is spent on. Every attempt object names the one it belongs to, so a single feed always says which kind it was.
Nothing is charged on any refusal:
| Code | Meaning |
|---|---|
insufficient_funds | The player cannot fund the entry fee |
entries_closed | The round is not open, or the cutoff has passed |
attempt_cap_reached | This player has used their ten attempts in the round |
blocked | This player may not play for money: their own responsible gaming decision, or a state Challenge does not serve |
insider_entry | The account behind this player_ref is linked to your developer account |
not_found | Unknown round, or one belonging to another program |
insider_entry is the one that surprises people, so it is worth stating plainly: an account linked to your developer account cannot enter your own live tournament. That covers your developer login's own player account, anyone on your team, and your sandbox test accounts. Your game reports the scores that decide the ranks and the purse is Challenge's money, so an account on your side of that cannot also compete for it. Test against the sandbox twin with your test key, where entry is free and the whole round still runs.
Reporting a score
Identical to a Challenge. The same endpoint, the same header, the same body, and no flag anywhere that says which kind of attempt it was.
# The key lives on your server. It never reaches the browser.
curl -X POST https://api.withchallenge.com/v1/attempts/<<ATTEMPT_ID>>/outcome \
-H "X-API-Key: $CHALLENGE_API_KEY" \
-H "Idempotency-Key: <<ATTEMPT_ID>>" \
-H "Content-Type: application/json" \
-d '{ "score": 41200 }'A tournament attempt has no bar and no prize of its own, so the response comes back with met_threshold: false, threshold_applied: null and award: null. The score is recorded, the player's entry keeps it if it beats their previous best, and money moves at settlement by rank rather than here.
A report that arrives after the round stopped accepting scores answers round_closed, and costs the player nothing: that attempt is expired and refunded in the first step of settlement.
Settlement
Fifteen minutes after a round closes, in a fixed order:
- Expire and refund unplayed attempts. Any attempt still without a score is expired and its fee returned in full. Only players who actually played can be ranked.
- Cancel if the round cannot run. Fewer scored attempts than the minimum, fewer scored players than the minimum, or a program that has been suspended, and the round is unwound instead of paid. Every attempt is refunded in full and every entry reports
payout_status: "refunded". - Rank and pay the frozen table. Three positions, the amounts the round opened with, cents allocated so the three sum to the purse exactly.
Settlement is resumable. A crash mid-payout leaves the round settling and the next sweep re-runs it from the top, moving nothing twice.
The midpoint, where a round can end early
A round can also end at its halfway mark. If fewer than 24 scored attempts have been played by then, about a third of the way to the gate of 70, the round is cancelled there and every fee is refunded in full, exactly as it would have been at the close.
It is the same cancellation, a day earlier. A round that thin at half time was not going to reach 70 in the quieter half of its window, and leaving it open parks everyone who did pay in something they cannot win and cannot leave. A player refunded on the first evening can enter the next round with the same money.
Three things the check never does: it never runs on a round with no attempts gate, it never runs once the round has closed, since the close judges the round on the real gate, and it never runs twice, because a cancelled round is final. Rounds cancelled this way carry the reason insufficient_attempts_midpoint and an early_cancel_at field with the threshold they missed.
An entrant with an open fraud alert or enforcement case has their payout marked held. Everyone else on the table is paid on time, and disposition is a human decision, never an automatic forfeit.
Telling your side
Your server hears settlement on two webhooks. Register the endpoint in the Developer Portal and verify the signature before trusting a payload.
| Event | When | Carries |
|---|---|---|
tournament.settled | The round paid | The round, the entrant count, the positions paid, and every ranked entrant with their rank, payout and payout status |
tournament.cancelled | The round was unwound, at its close or at its midpoint | The reason (insufficient_attempts, insufficient_attempts_midpoint, insufficient_entrants or program_suspended), both minimums, the scored attempts, the scored entrants, and how many attempts were refunded. A midpoint cancellation adds early_cancel_at, the threshold it missed, and checked_at: "midpoint" |
One event covers both moments, so nothing new has to be handled: a round can simply disappear a day earlier than its close.
attempt.settled fires for tournament attempts exactly as it does for Challenge attempts, and carries tournament_round_id so one feed can tell the two apart.
Players are told on their own WebSocket channel, with their own rank, payout and payout status, so no relay of yours is involved. The island renders the round card, the field against the minimum, the countdown, the player's standing and the leaderboard itself, and on a settled tournament attempt it shows the score and the rank rather than a win or a miss, because the money is decided when the round closes.
For a board of your own, GET /v1/tournaments returns each tournament with the round it is running, and GET /v1/tournaments/rounds/{round_id}/leaderboard returns the standings: live while the round is open, final once it has settled, ranked by player_ref so you can reconcile against your own side. Both are documented in the API reference.
The board is enter to see
Ranks and scores go only to a player who has paid for an attempt in that round. Before their first paid attempt a player reads everything the round promises, the purse, the three prize amounts, the entry fee, the scored field against its minimum and the time left, and none of what the field has scored.
The leaderboard endpoint therefore takes an optional player_ref. Name the player you are rendering for and you get their board if they have entered. Without one, or for a player who has not entered this round, you get the masked shape: a 200 carrying locked: true, reason: "enter_to_see", the round object and the round's promises, and no data array. It is not an error and does not need error handling; it is what the pre-entry surface is made of.
Entering buys a neighborhood, not the whole table. While the round is live, the rows are that player's own row with one rank either side, and nothing else: not even the leaders, whose scores are the ones that read as a reason to stop. The rank above is always in view, so the next target is always concrete, and the paid positions surface on their own as a player climbs next to them. A player who has paid but not yet scored gets no rows until their first score lands. scored_players says how long the full table is, and the rank numbers say where the visible rows sit. The complete table arrives when the round settles.
This applies to the island's own board as well, so the rule is the same wherever a player reads it. Ask for the board with a player_ref and render whichever of the two shapes comes back.
Under a house-funded purse this is a solvency rule rather than a taste. What underwrites the prize is turnout, so a player who is put off entering does not shrink the purse, they raise the chance the round cancels and everybody is refunded. A visible board is a list of reasons not to enter. Everything that is a promise stays visible; what is behind the fee is the part that is a threat.
entrant_count against min_entrants, and the round's scored attempts against min_scored_attempts, are the round's open questions, and both are worth surfacing in your game. "8 of 12 players" is a call to action in a way that "8 players" is not, and the attempts line is the one that decides whether the purse is paid.
Related
- Challenges: the other thing an attempt can be for
- Programs and keys: the tier that decides your entry fee allowlist
- API: the
/v1reference, including every tournament payload
Source: https://www.withchallenge.com/docs/tournaments.md Docs index for AI agents: https://www.withchallenge.com/llms.txt
Ready to integrate?
Put a Bounty in your game in minutes.
Challenge for Players
Create an account, deposit funds, and compete for real money in games you already play.
Challenge for Developers
Add Challenge to your game and start earning from every match. Integration takes minutes.