Challenge
Docs>Build>Server-created attempts

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.

DoorWho authenticatesUse it when
Challenge.startAttempt()The player's own session, in the browserThe player is in your game with the island in front of them. This is the usual case
POST /v1/attemptsYour API key, with a player_refYour 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.

AnswerMeaning
201Linked. Repeating the call with the same pair is safe and returns the same answer
404 not_foundNo Challenge account matches that email. Send the player to sign up, then call again
400 invalid_requestplayer_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" }'
FieldRequiredNotes
challenge_idone of twoPlaying a fixed bar for a fixed prize
tournament_round_idone of twoBuying a place on a round's rank table
player_refyesMust already be linked
metadatanoAn 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

CodeHTTPCause
invalid_request400Both parents named, neither named, player_ref missing, or no Idempotency-Key
unauthorized401The key is wrong or for the other environment
insufficient_funds402The player's balance does not cover the fee. Nothing was charged
not_found404Unknown Challenge, a Challenge belonging to another program, or a player_ref that is not linked
challenge_not_available409The Challenge is suspended or closed, or its prize exceeds your tier ceiling
challenge_already_claimed409This player has already won this Challenge. Permanent for the pair
rung_locked409A ladder rung whose parent this player has not claimed
attempt_cap_reached409Ten paid attempts on this Challenge today, or the round's own cap
entries_closed409The tournament round is closed or past its entry cutoff
idempotency_key_reused409The 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}/outcome

Same 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


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.