Challenges.API

Documentation

All 126 operations 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.

Open 10

No key at all.

Meta
GET/v1/statusService identity and the capabilities this instance has
GET/v1/openapi.jsonThis document, describing every endpoint of this instance
GET/v1/healthHealth, with the checks actually performed and the queue depths
Recovery
GET/v1/auth/recovery/confirmConfirm a rescue address from its email
Competition
GET/v1/titles/:id/card.svgA title as a shareable image
Console
GET/v1/dev/auth/githubStart signing in with GitHub
GET/v1/dev/auth/github/callbackFinish GitHub sign-in and set a session cookie
POST/v1/dev/auth/emailSend a magic link and a six-digit code
GET/v1/dev/auth/email/callbackSign in from the emailed link
POST/v1/dev/auth/email/verifySign in with the emailed code

Client 19

An app key, public or secret. Safe in a game build.

Identity
POST/v1/auth/anonymousCreate an account in one call, no personal data required
POST/v1/auth/redeemRedeem a link code on another device or app
GET/v1/playersFind other players by the start of their handle
GET/v1/players/:handlePublic profile: titles and badges only
GET/v1/catalogDisciplines, regions, season and collections of this app
Recovery
POST/v1/auth/passkey/challengeStart signing in with a passkey
POST/v1/auth/passkeySign in on a device that never held a token
POST/v1/auth/recoverAsk for a recovery link; the answer never says whether one was sent
GET/v1/auth/recover/callbackRecover an account and end every other session
Competition
GET/v1/leaderboards/:disciplineA board, by region, friends or globally; cursor paged
GET/v1/daily/:disciplineThe day seed, identical worldwide
GET/v1/ratings/:disciplineGlicko-2 ratings for a head-to-head discipline
GET/v1/titlesThe title archive
Ceremony
GET/v1/tournamentsTournaments this app has run or is running
GET/v1/tournaments/:slugThe whole bracket, drawable from this alone
GET/v1/ghosts/:disciplineThe runs at the top, with a link to each trace
GET/v1/ghosts/trace/:entryThe bytes one verified run was made of
Access
GET/v1/regions/resolveA position becomes a district; the position itself is not stored
GET/v1/waitlistClosed regions, ordered by demand

Player 41

An app key plus a signed-in player. Anything tied to a person.

Identity
POST/v1/me/link-codeMint a one-time code to carry this identity elsewhere
GET/v1/meOwn profile: qualifications, badges, titles, region
GET/v1/me/exportEverything stored about this player, as JSON
DELETE/v1/meDelete the account, irreversibly and completely
PATCH/v1/me/regionChoose a home district; locked for the season
PATCH/v1/me/profileDisplay name, avatar, locale, featured title or badge
PATCH/v1/me/handleChange the handle; locked for 30 days afterwards
Recovery
POST/v1/me/passkeys/challengeStart adding a passkey to this account
POST/v1/me/passkeysRegister a passkey for this account
GET/v1/me/passkeysThe passkeys that can sign in to this account
DELETE/v1/me/passkeys/:idRemove one passkey from this account
GET/v1/me/sessionsSessions currently able to act as this player
POST/v1/me/sessions/revoke-othersEnd every session but this one
POST/v1/me/recovery-emailAttach an optional rescue address
DELETE/v1/me/recovery-emailRemove the rescue address
Competition
POST/v1/entriesSubmit one entry, with a trace where the discipline verifies
GET/v1/entries/:idWhat became of one entry, including a verdict
GET/v1/disciplines/:discipline/meOwn value, rank, streak and exam status
GET/v1/leaderboards/:discipline/aroundThe rows immediately around you
POST/v1/challengesChallenge somebody, or leave an open invitation
POST/v1/challenges/:id/acceptAccept a challenge that was aimed at you
GET/v1/challengesChallenges you are part of, open and settled
GET/v1/collections/:slugA collection with your own holdings
Social
POST/v1/me/follows/:handleFollow a rival, without asking their permission
DELETE/v1/me/follows/:handleStop following somebody
GET/v1/me/followsThe people whose scores you want to see next to yours
POST/v1/me/blocks/:handleBlock a person; cuts contact, never results
DELETE/v1/me/blocks/:handleLift a block and allow contact again
GET/v1/me/blocksPeople you have cut contact with
POST/v1/reportsReport a person to the operator
Live
GET/v1/eventsOwn events, cursor based
GET/v1/events/streamThe same events as SSE, resumable via Last-Event-ID
POST/v1/me/presenceSay you are around; expires after 90 seconds
GET/v1/presenceHow many are online, and which of them are your rivals
POST/v1/queueEnter matchmaking and get a ticket to poll
GET/v1/queue/:ticketPoll a ticket; carries a join ticket once matched
DELETE/v1/queue/:ticketLeave the queue before anybody was matched
Ceremony
POST/v1/tournaments/:slug/joinEnter a tournament you have qualified for
Access
POST/v1/me/invitesSpend one invite from your own allowance
GET/v1/me/invitesInvite allowance and who joined through you
POST/v1/waitlist/:regionWait for a closed region; it opens itself at its threshold

Server 20

The secret app key. Belongs on your server, never in a client.

Developer
POST/v1/disciplinesCreate a discipline, its aggregation and its exam
POST/v1/disciplines/:slug/verifierAttach or detach a verifier module
POST/v1/badgesCreate a badge scoped to this app
POST/v1/collectionsCreate a collection of things to find
POST/v1/collections/:slug/itemsDefine the items a collection is made of
POST/v1/collections/:slug/grantGrant a collectible to a player
POST/v1/matchesReport a duel result; updates Glicko-2
POST/v1/invitesMint invite codes for a closed app
POST/v1/tournamentsCreate a tournament for one discipline
POST/v1/tournaments/:slug/startSeed and start the bracket
POST/v1/tournaments/:slug/matches/:id/resultDecide one tournament match
POST/v1/verifier/modulesUpload a WebAssembly verifier module
GET/v1/verifier/modulesVerifier modules this app has uploaded
GET/v1/verifier/usageVerified runs and CPU time, per day
GET/v1/signing-secretThe secret that verifies join tickets and signs webhooks
POST/v1/tickets/verifyCheck a join ticket online instead of offline
POST/v1/webhooksRegister an endpoint to be told rather than asked
GET/v1/webhooksWebhook endpoints, with delivery health
DELETE/v1/webhooks/:idSwitch a webhook endpoint off for good
GET/v1/webhooks/:id/deliveriesWhat was sent, what failed, and why

Console 11

A developer session cookie. Your account, your apps, your keys.

Console
POST/v1/dev/logoutEnd this console session
GET/v1/dev/meDeveloper account and app quota
GET/v1/dev/sessionsBrowser sessions signed in to this console
POST/v1/dev/sessions/revoke-othersEnd every console session but this one
POST/v1/dev/appsCreate an app and its first key pair
GET/v1/dev/appsThe apps this developer account owns
GET/v1/dev/apps/:slug/keysKeys, with last use and revocation
POST/v1/dev/apps/:slug/keysMint a key; rotation happens without a gap
POST/v1/dev/keys/:id/revokeRevoke a key, never the last of its kind
GET/v1/dev/apps/:slug/usageThis month against the free allowance; nothing is charged
GET/v1/dev/auditWhat this developer did, in order

Operator 25

The admin key. Whoever runs this instance, across every app.

Operator
POST/v1/admin/appsCreate an app and show its keys once
PATCH/v1/admin/apps/:slugAccess mode and invite allowance
GET/v1/admin/appsEvery app, with player and entry counts
GET/v1/admin/apps/:slugDisciplines, activity and review cases
POST/v1/admin/regionsAdd a region, open or closed with a threshold
POST/v1/admin/regions/:id/unlockOpen a region and notify its waitlist
GET/v1/admin/regions/densityContenders per region, the number titles hang on
POST/v1/admin/seasonsCreate a season for competition to happen in
GET/v1/admin/seasonsSeasons with entry and title counts
GET/v1/admin/seasonThe season currently open for entries
POST/v1/admin/seasons/:id/closeAward titles and open the next season
POST/v1/admin/entries/:id/reviewDecide an entry that was held for review
GET/v1/admin/reportsReports waiting for an operator decision
POST/v1/admin/reports/:id/resolveRename, suspend or ban
POST/v1/admin/players/:handle/statusSet a player status directly
POST/v1/admin/badgesCreate a platform-wide badge
GET/v1/admin/eventsRecent events across every app
GET/v1/admin/invitesInvite codes handed out across the platform
POST/v1/admin/maintenanceRetention sweep; meant for a daily cron
POST/v1/admin/standings/rebuildRebuild the standings projection from the ledger
POST/v1/admin/webhooks/retryRetry deliveries that are due
Verifier
POST/v1/verifier/claimClaim a batch of verification jobs to run
GET/v1/verifier/blob/:kind/:keyFetch a module or a trace to re-simulate
POST/v1/verifier/jobs/:id/resultReport verified, failed or error
GET/v1/verifier/jobsVerification jobs and what became of them

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.