---
title: "Programs and keys"
description: "The program as the unit every request is scoped to: the public program ID the browser gets, the sk_test_ and sk_live_ keys your server gets, why every key failure answers one 401, approval, integrity tiers and rotation."
url: https://www.withchallenge.com/docs/programs-and-keys
canonical_markdown: https://www.withchallenge.com/docs/programs-and-keys.md
docs_index: https://www.withchallenge.com/llms.txt
api_version: v1
lastUpdated: 2026-09-22T16:56:18.000Z
---

# 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

| Value | Example | Where it belongs | What it does |
|---|---|---|---|
| Program ID | `<<YOUR_PROGRAM_ID>>` | The browser. Public. | Names your program to Challenge.js so the island knows what to offer |
| Test key | `sk_test_...` | Your server, sandbox | Authenticates `/v1` against sandbox. No approval needed |
| Live key | `sk_live_...` | Your server, production | Authenticates `/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.

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

**Runs on your server.**

```javascript
// 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](https://www.withchallenge.com/docs/sandbox.md).

## Every refusal answers 401

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

```json
{ "error": { "code": "unauthorized", "message": "Invalid API key." } }
```

| Cause | What to check |
|---|---|
| No `X-API-Key` header | The 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 key | The key was rotated, or copied from a different program |
| The key is right but for the other environment | `sk_test_` reaches sandbox only, `sk_live_` production only |
| An `sk_live_` key on a program that is not approved | Approval, below |
| The program is suspended or closed, or the account is scheduled for deletion | The 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](https://www.withchallenge.com/docs/sandbox.md) 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.

| Tier | Prize ceiling | What backs a score |
|---|---|---|
| C | $100 | Partner reported, authenticated by an API key. The default. |
| B | $1,000 | A reviewed partner, the same reporting path, a larger ceiling |
| A | $10,000 | Verified 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](https://www.withchallenge.com/docs/calibration.md).
- **How high a ladder goes.** Rungs double the prize, so the tier ceiling is what stops the ladder. See [The Challenge ladder](https://www.withchallenge.com/docs/challenge-ladder.md).
- **Which entry fees a tournament may charge.** Tier C: $1. Tier B: $1 or $3. Tier A: $1, $3 or $5. See [Tournaments](https://www.withchallenge.com/docs/tournaments.md).

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.

| Surface | Program ID | `sk_test_` | `sk_live_` |
|---|---|---|---|
| `Challenge.init({ program })` in page JavaScript | Yes | No | No |
| A client bundle, minified or not | Yes | No | No |
| `NEXT_PUBLIC_*`, `VITE_*` or any client-exposed variable | Yes | No | No |
| A mobile app binary or a shipped game build | Yes | No | No |
| Your server's environment (`process.env.CHALLENGE_API_KEY`) | Not needed | Sandbox | Production |
| A serverless function or edge function environment | Not needed | Sandbox | Production |
| A CI secret store | Not needed | Yes | Yes |
| A committed file, anywhere | Yes | No | No |
| A support ticket, an issue, a chat message | Yes | No | No |

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](https://www.withchallenge.com/docs/trust-model.md): why the browser may start an attempt and may not end one
- [Sandbox](https://www.withchallenge.com/docs/sandbox.md): testing with a test key, and what production does differently
- [API](https://www.withchallenge.com/docs/api.md): 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
