Challenges.API

Documentation

Everything under /v1.

Responses are JSON. Errors carry an error field in plain words, not a code you have to look up.

The machine-readable description is at/v1/openapi.json — OpenAPI 3.1, and a test fails the build if it ever describes an endpoint this service does not have, or misses one it does.

Try it

A real account, on a real instance, right now.

This runs against a sandbox app on the live service. The key below is a public key, and public keys are meant to be public: it can read boards and create throwaway accounts, and nothing that carries authority.

Not signed in.

Press a button. Every call is shown exactly as it goes out.

Five rules that explain most answers

  1. The ledger is the truth.Leaderboards, qualifications, streaks, badges, ratings and titles are derived.
  2. A qualification is the entrance.No exam passed, no place on a board.
  3. A title never reaches higher than its trust tier.Tier 0 awards nothing, tier 1 reaches city level at most.
  4. Two keys per app.pk_ may live in a client, sk_ never.
  5. A block cuts contact, never results.Otherwise a leaderboard would have different truths per viewer.

Authentication

headerwhounlocks
X-App-Key: pk_...any clientaccounts, catalog, entries, leaderboards, social
X-App-Key: sk_...your serverdisciplines, duels, collectibles, invites
Authorization: Bearera playeranything tied to a person
X-Admin-Keythe operatorapps, regions, seasons, moderation, retention

A player token is platform-wide: the same token identifies the same person in every app. To move an identity to another device or domain, mint a one-time code with POST /v1/me/link-code and redeem it with POST /v1/auth/redeem.

Submitting an entry

POST /v1/entries
X-App-Key: pk_live_...
Authorization: Bearer ...

{
  "discipline": "kilometres",
  "value": 7.2,
  "occurred_at": "2026-08-22T21:40:00+02:00",
  "idem_key": "run-2026-08-22-a"
}

Habit apps, read this: the day slice comes from occurred_at. Send the timestamp with its local offset and 23:40 local falls on the right day instead of the UTC day after. Without it, server time in UTC applies.

{
  "entry_id": "ent_...",
  "value": 7.2,
  "aggregate": 107.2,
  "aggregation": "sum",
  "qualified": true,
  "qualified_now": false,
  "rank": { "region": { "rank": 2, "of": 9 },
            "global": { "rank": 41, "of": 260 } },
  "streak_days": null,
  "settled_challenges": [],
  "badges_earned": []
}

Returns 202 with status: "review" above the plausibility limit, 200 with duplicate: truefor a repeated idem_key, 413 whenmeta exceeds 4 KB.

Client X-App-Key: pk_… + Bearer

Everything that may live in a browser or a game build.

POST/v1/auth/anonymousaccount in one call (invite_code when the app is closed)
POST/v1/me/link-codeone-time code to move the identity
POST/v1/auth/redeemredeem it on another device or app
GET/v1/meprofile, qualifications, badges, titles
GET/v1/me/exportcomplete data export
DELETE/v1/meirreversible deletion
GET/v1/regions/resolve?lat= ?lon= — which district a position is in
PATCH/v1/me/regionhome district, locked for the season
PATCH/v1/me/profiledisplay name, avatar, locale, featured title or badge
PATCH/v1/me/handlechange handle, 30-day lock afterwards
GET/v1/catalogdisciplines, regions, season, collections
POST/v1/entriessubmit an entry
GET/v1/disciplines/:d/meown value, rank, streak, exam status
GET/v1/leaderboards/:d?region= ?scope=friends ?limit=
GET/v1/daily/:dthe day seed, identical worldwide
POST/v1/challengescreate an async challenge
POST/v1/challenges/:id/acceptaccept one
GET/v1/challengesyour challenges
GET/v1/ratings/:drating list
GET/v1/collections/:sluga collection with your holdings
POST/v1/me/follows/:handlefollow a rival
POST/v1/me/blocks/:handleblock a person
POST/v1/reportsreport a person
POST/v1/waitlist/:regionwait for a closed region
GET/v1/eventsevent stream (?since=)

Developer X-App-Key: sk_…

Everything that carries authority. Belongs on a server, never in a client.

POST/v1/disciplinesaggregation, trust tier, exam bar, title reach
POST/v1/badgesa badge scoped to your app
POST/v1/collectionscreate a collection
POST/v1/collections/:slug/itemsdefine collectibles
POST/v1/collections/:slug/grantgrant one to a player
POST/v1/matchesduel result with placements, updates Glicko-2
POST/v1/invitesinvite codes, shown exactly once

Developer console session cookie

Sign-in authenticates the console, never an API request. A game presents a key and nothing else, so an outage at the sign-in provider cannot take a game down.

GET/v1/dev/auth/githubstart GitHub sign-in (?redirect= back to your console)
POST/v1/dev/auth/emailsend a magic link and a six-digit code
POST/v1/dev/auth/email/verifythe code, when the mail opens on another device
GET/v1/dev/meaccount, two-factor status, app quota
GET/v1/dev/sessionslist sessions; revoke-others ends the rest
POST/v1/dev/appscreate your own app, keys included
GET/v1/dev/apps/:slug/keysprefix, name, last used, expiry, revocation
POST/v1/dev/apps/:slug/keysmint a key (kind, name, expires_in_days)
POST/v1/dev/keys/:id/revokerevoke one, with a reason
GET/v1/dev/auditwhat you did, in order

Operator X-Admin-Key

Platform operations: apps, regions, seasons, moderation, retention.

POST/v1/admin/appscreate an app, both keys shown once
PATCH/v1/admin/apps/:slugaccess mode, invite allowance
GET/v1/admin/apps/:slugdisciplines, activity, review cases
POST/v1/admin/regionsunlock a region, optionally closed with a threshold
POST/v1/admin/regions/:id/unlockopen it and notify the waitlist
GET/v1/admin/regions/densitycontenders per region (?season=)
POST/v1/admin/seasons/:id/closeaward titles, open the next (?dry_run=1)
POST/v1/admin/entries/:id/reviewdecide a held entry
GET/v1/admin/reportsmoderation queue
POST/v1/admin/reports/:id/resolverename, suspend or ban
POST/v1/admin/maintenanceretention sweep (?dry_run=1)

Keys, and what happens when one leaks

A key is an object with a life, not a column on your app. It is shown once, only its hash and prefix are stored, and it records when it was last seen.

Rotation without a gap

Mint, deploy, then revoke. Both keys work in between — the only kind of rotation anyone actually performs. Revoking the last live key of a kind is refused, because that is not rotation, that is an outage.

A second, recent proof for secrets

Minting a secret key needs two-factor at the provider for a GitHub account, or a sign-in from the last 15 minutes for an email account. Public keys stay available either way, so nobody is locked out of their own app.

Two ways in, no password

GitHub, or an emailed link and code. The email path answers the same for known and unknown addresses, expires in 15 minutes, works once, and kills the code after five wrong guesses.

A revoked key says so

It answers 401 with app key revoked and a timestamp, instead of a silent failure that costs somebody an hour.

Distinctive prefixes

chapi_sk_ is deliberately greppable so a secret scanner can recognise it in a public repository. Registering the pattern with GitHub is phase 1b on the roadmap.

Trust tiers

A leaderboard is worth what its weakest claim is worth. Every discipline declares who vouches for a result, and the ladder is capped by it.

tiervouched for byhighest title
0 clientnobodynone
1 replayserver re-runs the input tracecity
2 serverthe app's own signing serverworld
3 witnesseda scheduled, recorded finalworld

Disciplines at tier 2 and above refuse entries submitted with the public key. Tier 1 verification itself is phase 6 on theroadmap — until then, tier 1 means the shape is ready, not that a run has been re-simulated.