Challenge
Docs>Concepts>Programs and keys

Programs and keys

A program is your game as Challenge knows it, and it is the boundary every request is scoped to: the program ID identifies it in public, the secret key authenticates it in private.

The program is the tenancy unit

One program is one integrated product. It owns the Challenges you create, the tournaments you run, the players you have linked, the attempts they buy, the webhook endpoints you register and the integrity tier that caps what you may pay.

A key resolves directly to one program, so there is no "which of this developer's programs did they mean" question for a request to answer wrongly. Everything inside /v1 reads that resolved program and nothing else. A Challenge id, attempt id or round id that belongs to another program answers not_found rather than unauthorized, because confirming that a resource exists somewhere else is itself a disclosure.

If you run two products, run two programs. There is no limit on how many Challenges one program may hold, so a second Challenge is not a reason to open a second program: a racing game with a lap time and a drift score is one program with two Challenges.

Two values, two audiences

ValueExampleWhere it belongsWhat it does
Program ID<<YOUR_PROGRAM_ID>>The browser. Public.Names your program to Challenge.js so the island knows what to offer
Test keysk_test_...Your server, sandboxAuthenticates /v1 against sandbox. No approval needed
Live keysk_live_...Your server, productionAuthenticates /v1 against production. Issued on approval

The program ID is not a credential. It appears in your page source, in your bundle and in every browser's network tab, and that is fine: it names a program, it cannot start an attempt, and it cannot move money. Starting an attempt requires the player's own session, which the island holds.

An sk_ key is the opposite. It can report a score, and a score is what pays a prize, so a key in a client bundle is a key anyone can read and use to settle attempts as you.

// The browser. The program ID is public and belongs here. Challenge.init({ program: '<<YOUR_PROGRAM_ID>>' })

Runs on your server.

// The key lives on your server. It never reaches the browser. const response = await fetch( `https://api.withchallenge.com/v1/attempts/${attemptId}/outcome`, { method: 'POST', headers: { 'X-API-Key': process.env.CHALLENGE_API_KEY, 'Idempotency-Key': attemptId, 'Content-Type': 'application/json', }, body: JSON.stringify({ score: 34 }), } ) const settled = await response.json() // { attempt_id, score, met_threshold, threshold_applied, recorded_at, receipt_id, award }

What a key resolves to

Every /v1 request carries X-API-Key. The prefix decides which column the key is looked up in: sk_test_ against the program's test key, sk_live_ against its live key. One value of each per program, so a key is either the current one or it is nothing.

The lookup also decides the environment. There is no ?mode= parameter and no environment header: sandbox and production are the same routes reached with different keys, and the key you sent is the whole answer to which one you are in.

Sandbox is a key, not a copy. Both keys are two values on one program record and resolve the same program, the same Challenges, the same linked players and the same attempts: there is no separate sandbox data set and nothing gets reset. What the test key changes is the approval gate, which it skips, and that nothing you do with it charges real money, because no Challenge is priced until Challenge prices one. See Sandbox.

Every refusal answers 401

Six different things can stop a key working, and all six answer the same status with the same shape:

{ "error": { "code": "unauthorized", "message": "Invalid API key." } }
CauseWhat to check
No X-API-Key headerThe header is required on every /v1 request
The value does not start with sk_test_ or sk_live_A truncated or misquoted environment variable
No program holds that keyThe key was rotated, or copied from a different program
The key is right but for the other environmentsk_test_ reaches sandbox only, sk_live_ production only
An sk_live_ key on a program that is not approvedApproval, below
The program is suspended or closed, or the account is scheduled for deletionThe Developer Portal says which

The first four deliberately share one message. Distinguishing "no such key" from "right key, wrong environment" would tell someone guessing which half of the guess was correct. The last two say more, because they are facts about your own account rather than about a secret.

The practical consequence: a 401 in production is far more often the wrong environment than a wrong key. Check which of the two keys your process actually loaded before you rotate anything.

Live keys require approval

A program is created with a test key, integrity tier C and approved: false. That is enough to build the whole integration: sandbox, calibrating Challenges, the island, the outcome endpoint, webhooks.

It is not enough to move real money. An sk_live_ key is issued when your program is approved, and a live key presented for an unapproved program answers 401 with "This program is not approved for live use." Approval is a review of the program, not a form, and the same review is what raises an integrity tier.

Nothing else changes at the swap. The same endpoints, the same payloads, the same error codes. See Sandbox for what differs between the two environments.

Integrity tiers

Your tier is Challenge's view of how well a result you report can be verified, and it is the dial that turns that view into money. Every program starts at C. A tier is raised by review, never granted at signup.

TierPrize ceilingWhat backs a score
C$100Partner reported, authenticated by an API key. The default.
B$1,000A reviewed partner, the same reporting path, a larger ceiling
A$10,000Verified independently of the partner: computed by Challenge, or pulled from an oracle you do not control. Arranged during review.

The tier caps four things:

  • The prize on a Challenge. Checked when the prize is paid, not only when the Challenge was priced, because a program can be demoted after its Challenges exist. Over the ceiling answers challenge_not_available and names the limit.
  • The provisional ceiling. A Challenge whose bar came from a claim rather than from observed play may offer at most 20% of your tier ceiling until real attempts confirm it. See Calibration.
  • How high a ladder goes. Rungs double the prize, so the tier ceiling is what stops the ladder. See The Challenge ladder.
  • Which entry fees a tournament may charge. Tier C: $1. Tier B: $1 or $3. Tier A: $1, $3 or $5. See Tournaments.

A tier is not a permission to report. Scores enter the money system from an authenticated partner server at every tier, and from nowhere else. What the tier decides is how much a payout may be worth on the strength of that report, and at tier A, that an independently verified source is required rather than optional.

Rotation

Rotating a test key is self-serve in the Developer Portal, on the program. The new value is returned once, and the old one stops working with the update: there is one test key per program, and the middleware resolves that one value. Anything still holding the old key starts answering 401 immediately, so roll your server's environment variable in the same change.

Rotate whenever a key has been where a key should not be: a committed .env, a shared log, a screenshot, a departed contractor's machine. Nothing about an attempt survives rotation, because keys authenticate requests rather than own resources.

The program ID is not rotatable and does not need to be. It is public by design.

Where each value may appear

This is the whole rule, and it is worth reading as a checklist before you ship.

SurfaceProgram IDsk_test_sk_live_
Challenge.init({ program }) in page JavaScriptYesNoNo
A client bundle, minified or notYesNoNo
NEXT_PUBLIC_*, VITE_* or any client-exposed variableYesNoNo
A mobile app binary or a shipped game buildYesNoNo
Your server's environment (process.env.CHALLENGE_API_KEY)Not neededSandboxProduction
A serverless function or edge function environmentNot neededSandboxProduction
A CI secret storeNot neededYesYes
A committed file, anywhereYesNoNo
A support ticket, an issue, a chat messageYesNoNo

If a live key has ever been in a client bundle, treat it as public and rotate it. There is no prize ceiling small enough to make a browser an acceptable place to keep one.

Related

  • The trust model: why the browser may start an attempt and may not end one
  • Sandbox: testing with a test key, and what production does differently
  • API: the /v1 reference, including the full error contract

Source: https://www.withchallenge.com/docs/programs-and-keys.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.