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
- The ledger is the truth.Leaderboards, qualifications, streaks, badges, ratings and titles are derived.
- A qualification is the entrance.No exam passed, no place on a board.
- A title never reaches higher than its trust tier.Tier 0 awards nothing, tier 1 reaches city level at most.
- Two keys per app.
pk_may live in a client,sk_never. - A block cuts contact, never results.Otherwise a leaderboard would have different truths per viewer.
Authentication
| header | who | unlocks |
|---|---|---|
| X-App-Key: pk_... | any client | accounts, catalog, entries, leaderboards, social |
| X-App-Key: sk_... | your server | disciplines, duels, collectibles, invites |
| Authorization: Bearer | a player | anything tied to a person |
| X-Admin-Key | the operator | apps, 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/status | Service identity and the capabilities this instance has |
| GET | /v1/openapi.json | This document, describing every endpoint of this instance |
| GET | /v1/health | Health, with the checks actually performed and the queue depths |
| Recovery | ||
| GET | /v1/auth/recovery/confirm | Confirm a rescue address from its email |
| Competition | ||
| GET | /v1/titles/:id/card.svg | A title as a shareable image |
| Console | ||
| GET | /v1/dev/auth/github | Start signing in with GitHub |
| GET | /v1/dev/auth/github/callback | Finish GitHub sign-in and set a session cookie |
| POST | /v1/dev/auth/email | Send a magic link and a six-digit code |
| GET | /v1/dev/auth/email/callback | Sign in from the emailed link |
| POST | /v1/dev/auth/email/verify | Sign in with the emailed code |
Client 19
An app key, public or secret. Safe in a game build.
| Identity | ||
| POST | /v1/auth/anonymous | Create an account in one call, no personal data required |
| POST | /v1/auth/redeem | Redeem a link code on another device or app |
| GET | /v1/players | Find other players by the start of their handle |
| GET | /v1/players/:handle | Public profile: titles and badges only |
| GET | /v1/catalog | Disciplines, regions, season and collections of this app |
| Recovery | ||
| POST | /v1/auth/passkey/challenge | Start signing in with a passkey |
| POST | /v1/auth/passkey | Sign in on a device that never held a token |
| POST | /v1/auth/recover | Ask for a recovery link; the answer never says whether one was sent |
| GET | /v1/auth/recover/callback | Recover an account and end every other session |
| Competition | ||
| GET | /v1/leaderboards/:discipline | A board, by region, friends or globally; cursor paged |
| GET | /v1/daily/:discipline | The day seed, identical worldwide |
| GET | /v1/ratings/:discipline | Glicko-2 ratings for a head-to-head discipline |
| GET | /v1/titles | The title archive |
| Ceremony | ||
| GET | /v1/tournaments | Tournaments this app has run or is running |
| GET | /v1/tournaments/:slug | The whole bracket, drawable from this alone |
| GET | /v1/ghosts/:discipline | The runs at the top, with a link to each trace |
| GET | /v1/ghosts/trace/:entry | The bytes one verified run was made of |
| Access | ||
| GET | /v1/regions/resolve | A position becomes a district; the position itself is not stored |
| GET | /v1/waitlist | Closed regions, ordered by demand |
Player 41
An app key plus a signed-in player. Anything tied to a person.
| Identity | ||
| POST | /v1/me/link-code | Mint a one-time code to carry this identity elsewhere |
| GET | /v1/me | Own profile: qualifications, badges, titles, region |
| GET | /v1/me/export | Everything stored about this player, as JSON |
| DELETE | /v1/me | Delete the account, irreversibly and completely |
| PATCH | /v1/me/region | Choose a home district; locked for the season |
| PATCH | /v1/me/profile | Display name, avatar, locale, featured title or badge |
| PATCH | /v1/me/handle | Change the handle; locked for 30 days afterwards |
| Recovery | ||
| POST | /v1/me/passkeys/challenge | Start adding a passkey to this account |
| POST | /v1/me/passkeys | Register a passkey for this account |
| GET | /v1/me/passkeys | The passkeys that can sign in to this account |
| DELETE | /v1/me/passkeys/:id | Remove one passkey from this account |
| GET | /v1/me/sessions | Sessions currently able to act as this player |
| POST | /v1/me/sessions/revoke-others | End every session but this one |
| POST | /v1/me/recovery-email | Attach an optional rescue address |
| DELETE | /v1/me/recovery-email | Remove the rescue address |
| Competition | ||
| POST | /v1/entries | Submit one entry, with a trace where the discipline verifies |
| GET | /v1/entries/:id | What became of one entry, including a verdict |
| GET | /v1/disciplines/:discipline/me | Own value, rank, streak and exam status |
| GET | /v1/leaderboards/:discipline/around | The rows immediately around you |
| POST | /v1/challenges | Challenge somebody, or leave an open invitation |
| POST | /v1/challenges/:id/accept | Accept a challenge that was aimed at you |
| GET | /v1/challenges | Challenges you are part of, open and settled |
| GET | /v1/collections/:slug | A collection with your own holdings |
| Social | ||
| POST | /v1/me/follows/:handle | Follow a rival, without asking their permission |
| DELETE | /v1/me/follows/:handle | Stop following somebody |
| GET | /v1/me/follows | The people whose scores you want to see next to yours |
| POST | /v1/me/blocks/:handle | Block a person; cuts contact, never results |
| DELETE | /v1/me/blocks/:handle | Lift a block and allow contact again |
| GET | /v1/me/blocks | People you have cut contact with |
| POST | /v1/reports | Report a person to the operator |
| Live | ||
| GET | /v1/events | Own events, cursor based |
| GET | /v1/events/stream | The same events as SSE, resumable via Last-Event-ID |
| POST | /v1/me/presence | Say you are around; expires after 90 seconds |
| GET | /v1/presence | How many are online, and which of them are your rivals |
| POST | /v1/queue | Enter matchmaking and get a ticket to poll |
| GET | /v1/queue/:ticket | Poll a ticket; carries a join ticket once matched |
| DELETE | /v1/queue/:ticket | Leave the queue before anybody was matched |
| Ceremony | ||
| POST | /v1/tournaments/:slug/join | Enter a tournament you have qualified for |
| Access | ||
| POST | /v1/me/invites | Spend one invite from your own allowance |
| GET | /v1/me/invites | Invite allowance and who joined through you |
| POST | /v1/waitlist/:region | Wait 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/disciplines | Create a discipline, its aggregation and its exam |
| POST | /v1/disciplines/:slug/verifier | Attach or detach a verifier module |
| POST | /v1/badges | Create a badge scoped to this app |
| POST | /v1/collections | Create a collection of things to find |
| POST | /v1/collections/:slug/items | Define the items a collection is made of |
| POST | /v1/collections/:slug/grant | Grant a collectible to a player |
| POST | /v1/matches | Report a duel result; updates Glicko-2 |
| POST | /v1/invites | Mint invite codes for a closed app |
| POST | /v1/tournaments | Create a tournament for one discipline |
| POST | /v1/tournaments/:slug/start | Seed and start the bracket |
| POST | /v1/tournaments/:slug/matches/:id/result | Decide one tournament match |
| POST | /v1/verifier/modules | Upload a WebAssembly verifier module |
| GET | /v1/verifier/modules | Verifier modules this app has uploaded |
| GET | /v1/verifier/usage | Verified runs and CPU time, per day |
| GET | /v1/signing-secret | The secret that verifies join tickets and signs webhooks |
| POST | /v1/tickets/verify | Check a join ticket online instead of offline |
| POST | /v1/webhooks | Register an endpoint to be told rather than asked |
| GET | /v1/webhooks | Webhook endpoints, with delivery health |
| DELETE | /v1/webhooks/:id | Switch a webhook endpoint off for good |
| GET | /v1/webhooks/:id/deliveries | What was sent, what failed, and why |
Console 11
A developer session cookie. Your account, your apps, your keys.
| Console | ||
| POST | /v1/dev/logout | End this console session |
| GET | /v1/dev/me | Developer account and app quota |
| GET | /v1/dev/sessions | Browser sessions signed in to this console |
| POST | /v1/dev/sessions/revoke-others | End every console session but this one |
| POST | /v1/dev/apps | Create an app and its first key pair |
| GET | /v1/dev/apps | The apps this developer account owns |
| GET | /v1/dev/apps/:slug/keys | Keys, with last use and revocation |
| POST | /v1/dev/apps/:slug/keys | Mint a key; rotation happens without a gap |
| POST | /v1/dev/keys/:id/revoke | Revoke a key, never the last of its kind |
| GET | /v1/dev/apps/:slug/usage | This month against the free allowance; nothing is charged |
| GET | /v1/dev/audit | What this developer did, in order |
Operator 25
The admin key. Whoever runs this instance, across every app.
| Operator | ||
| POST | /v1/admin/apps | Create an app and show its keys once |
| PATCH | /v1/admin/apps/:slug | Access mode and invite allowance |
| GET | /v1/admin/apps | Every app, with player and entry counts |
| GET | /v1/admin/apps/:slug | Disciplines, activity and review cases |
| POST | /v1/admin/regions | Add a region, open or closed with a threshold |
| POST | /v1/admin/regions/:id/unlock | Open a region and notify its waitlist |
| GET | /v1/admin/regions/density | Contenders per region, the number titles hang on |
| POST | /v1/admin/seasons | Create a season for competition to happen in |
| GET | /v1/admin/seasons | Seasons with entry and title counts |
| GET | /v1/admin/season | The season currently open for entries |
| POST | /v1/admin/seasons/:id/close | Award titles and open the next season |
| POST | /v1/admin/entries/:id/review | Decide an entry that was held for review |
| GET | /v1/admin/reports | Reports waiting for an operator decision |
| POST | /v1/admin/reports/:id/resolve | Rename, suspend or ban |
| POST | /v1/admin/players/:handle/status | Set a player status directly |
| POST | /v1/admin/badges | Create a platform-wide badge |
| GET | /v1/admin/events | Recent events across every app |
| GET | /v1/admin/invites | Invite codes handed out across the platform |
| POST | /v1/admin/maintenance | Retention sweep; meant for a daily cron |
| POST | /v1/admin/standings/rebuild | Rebuild the standings projection from the ledger |
| POST | /v1/admin/webhooks/retry | Retry deliveries that are due |
| Verifier | ||
| POST | /v1/verifier/claim | Claim a batch of verification jobs to run |
| GET | /v1/verifier/blob/:kind/:key | Fetch a module or a trace to re-simulate |
| POST | /v1/verifier/jobs/:id/result | Report verified, failed or error |
| GET | /v1/verifier/jobs | Verification 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.
| tier | vouched for by | highest title |
|---|---|---|
| 0 client | nobody | none |
| 1 replay | server re-runs the input trace | city |
| 2 server | the app's own signing server | world |
| 3 witnessed | a scheduled, recorded final | world |
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.