Server-created attempts
The second door into an attempt, for a game with no browser client at all.
Runs on your server
Read this page only if the answer to "where does the island render?" is "nowhere". A native mobile game with no web view, a console title, a headless simulation, a partner running their own storefront: those are the cases this exists for.
If your game runs in a browser, use Start a paid attempt instead and skip this page. Almost every integration should.
Two doors
Both are open, and both charge the fee and freeze the bar and the prize at creation.
| Door | Who authenticates | Use it when |
|---|---|---|
Challenge.startAttempt() | The player's own session, in the browser | The player is in your game with the island in front of them. This is the usual case |
POST /v1/attempts | Your API key, with a player_ref | Your server drives the whole flow and there is no island |
What you give up by taking the second door is everything the island renders. Challenge still runs accounts, verification, deposits, balances, responsible gaming limits and the ledger, but the surfaces that put those in front of a player live in Challenge.js. Without it, your player signs up, verifies, deposits and reads their balance in the Player Portal rather than inside your game, and you are the one who has to get them there.
There is still no arming endpoint. Arming is the player agreeing to a price on a surface Challenge owns, and the server door does not create one: it charges a fee against an account the player already funded, on a Challenge your server named.
Link the player first
Every attempt resolves a player_ref through a link row, and nothing else creates one. Without this call your server cannot create an attempt for anyone.
# The key lives on your server. It never reaches the browser.
curl -i -X POST "https://api.withchallenge.com/v1/players" \
-H "X-API-Key: $CHALLENGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"player_ref": "user_8134",
"email": "player@example.com"
}'{
"player_ref": "user_8134",
"linked": true,
"created_at": "2026-08-24T11:42:07.000Z"
}player_ref is your own identifier for the player, up to 64 characters. It is the only identity that ever crosses back to you: leaderboards and attempts are keyed on it, never on a Challenge user id and never on a Challenge username.
Challenge does not provision accounts from an API call. A player_ref is a string you chose, and treating it as authority to open an account that holds money would mint accounts with no email, no age verification and no consent. The player signs up with Challenge themselves; you link the account they already have.
| Answer | Meaning |
|---|---|
201 | Linked. Repeating the call with the same pair is safe and returns the same answer |
404 not_found | No Challenge account matches that email. Send the player to sign up, then call again |
400 invalid_request | player_ref or email missing |
The 404 deliberately does not distinguish "no such account" from anything else. Confirming which emails have Challenge accounts would let any partner enumerate Challenge's users.
No Idempotency-Key is required here. The link upserts on the pair, so repeating it is naturally safe.
Create the attempt
# The key lives on your server. It never reaches the browser.
curl -i -X POST "https://api.withchallenge.com/v1/attempts" \
-H "X-API-Key: $CHALLENGE_API_KEY" \
-H "Idempotency-Key: 3f0b7c21-9d84-4a6e-bb15-72c0e9d84a11" \
-H "Content-Type: application/json" \
-d '{
"challenge_id": "<<YOUR_CHALLENGE_ID>>",
"player_ref": "user_8134"
}'| Field | Required | Notes |
|---|---|---|
challenge_id | one of two | Playing a fixed bar for a fixed prize |
tournament_round_id | one of two | Buying a place on a round's rank table |
player_ref | yes | Must already be linked |
metadata | no | An object stored on the attempt and handed back unchanged |
Exactly one of challenge_id and tournament_round_id. Sending both, or neither, answers 400 invalid_request. They are not interchangeable: a Challenge attempt is judged against a fixed bar for a prize Challenge funds, and a tournament attempt buys a place on a rank table. A request naming both has not said which deal it wants, and picking one for you would be picking how your player's money is used. The same rule is enforced by a constraint on the attempts table, not only by the route.
Idempotency-Key is required. This call moves money.
The 201
{
"id": "7c1f0a92-4d6b-4b0e-9f3a-8b5c2d1e6f40",
"challenge_id": "a4e1c8b7-6f20-4d93-8c15-3b7e0a2f9d64",
"tournament_round_id": null,
"player_ref": "user_8134",
"status": "open",
"fee_charged": "2.00",
"created_at": "2026-08-24T11:59:12.000Z",
"metadata": {},
"outcome": null
}Exactly one of challenge_id and tournament_round_id is ever non-null, and which one it is says what this attempt is playing for. Money is a decimal string. outcome fills in once you report a score.
A replay under the same key returns the same 201 body with Idempotency-Replayed: true, charges nothing and, on a tournament round, consumes no cap slot.
The refusals
| Code | HTTP | Cause |
|---|---|---|
invalid_request | 400 | Both parents named, neither named, player_ref missing, or no Idempotency-Key |
unauthorized | 401 | The key is wrong or for the other environment |
insufficient_funds | 402 | The player's balance does not cover the fee. Nothing was charged |
not_found | 404 | Unknown Challenge, a Challenge belonging to another program, or a player_ref that is not linked |
challenge_not_available | 409 | The Challenge is suspended or closed, or its prize exceeds your tier ceiling |
challenge_already_claimed | 409 | This player has already won this Challenge. Permanent for the pair |
rung_locked | 409 | A ladder rung whose parent this player has not claimed |
attempt_cap_reached | 409 | Ten paid attempts on this Challenge today, or the round's own cap |
entries_closed | 409 | The tournament round is closed or past its entry cutoff |
idempotency_key_reused | 409 | The same key with a different body |
A Challenge belonging to another program answers not_found rather than a forbidden, because a forbidden would confirm it exists.
You can avoid most of the 409s by building your menu from GET /v1/challenges?player_ref=..., which returns exactly what this endpoint will accept for that player: Challenges they have already claimed are absent, and so are rungs they have not unlocked.
Reporting the outcome is identical
There is only one outcome endpoint and it does not care which door the attempt came through:
POST /v1/attempts/{attempt_id}/outcomeSame key, same Idempotency-Key convention, same response, same one-hour expiry. See Report the score from your server.
This door also runs calibrating Challenges
A calibrating Challenge has no bar, charges nothing and can pay nothing. The island will not arm one, because arming quotes a fee and a prize and a calibrating Challenge has neither to quote.
POST /v1/attempts accepts one. The attempt is created with fee_charged: "0.00", threshold_applied: null and prize_offered: 0, it settles as recorded rather than won or lost, and it contributes a score toward the Challenge's calibration sample.
That is the money-free loop. In sandbox it lets you run the whole sequence, create, report, settle, fetch, with nothing to undo afterwards, and in production it is how the observed calibration path collects its 1,000 adjudicated attempts from 200 distinct players. See Sandbox and Calibration.
The one thing it cannot do is pay. award is null on every calibrating attempt, and met_threshold is false, because there was no bar to meet.
Next
- Report the score from your server settles the attempt you just created.
- Programs and keys covers what a key may touch and what it never may.
- Tournaments covers entering rounds through this same endpoint.
- API reference has every
/v1endpoint in full.
Source: https://www.withchallenge.com/docs/server-attempts.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.