HTTP routes

Read this as a table of two columns

The strategy column is verification strategies, the scopes are dashboard scopes, refusals carry an error code, and what mounts them is the Worker contract.

A path, and the strategy that guards it. There is no implicit authentication anywhere in the kit, so every row below names one of five:

StrategyWho gets in
bearerA short-lived access token in Authorization: Bearer
sessionA cookie session, CSRF-guarded
signed-webhookThe sender’s own proof, over the exact bytes received
control-planeA scoped machine credential the customer issued. Default-denied
publicNobody is checked. A deliberate choice, written down

bearer and session are one guard in practice — requireAuth() accepts either, and the rows below say user where that guard runs. What separates them is the client, not the route.

Every base path is yours to move

The prefixes below are defaults. Each is a config key, and moving one moves every route under it.

CapabilityDefault prefix
Auth/auth
Payments/payments
Storage/storage
Media/media
Email/email
Vector/vector
Secrets/secrets
Support/support
Testers/testers
Audit/audit
Ledger/ledger
Leaderboard/leaderboard
Matchmaking/matchmaking
Multiplayer/multiplayer
Rating/rating
The control plane/control-plane

Auth

RouteStrategy
POST /auth/token/rotatepublic + CSRF — the refresh credential is the proof
GET /auth/devicesuser
POST /auth/devices/revokeuser + CSRF
GET /auth/admin/userscontrol-plane
GET /auth/admin/users/:userIdcontrol-plane
GET /auth/admin/devicescontrol-plane
POST /auth/admin/sessions/revokecontrol-plane
POST /auth/admin/users/:userId/sessions/revokecontrol-plane
POST /auth/admin/users/:userId/devices/revokecontrol-plane
ALL /auth/*Better Auth’s own surface

That last row is most of what a sign-in flow actually calls. Sign-in, sign-out, the OAuth callback, verification, /token, /jwks and session listing are Better Auth’s endpoints, mounted under the same prefix and answering in Better Auth’s own response shape — deliberately, because createAuthClient is a documented client surface and re-homing the body would strand every adopter reading error.code.

The admin routes are registered before the catch-all, or they would be dead: /auth/admin/users would reach Better Auth, which knows no such endpoint, and the 404 would come from the wrong layer entirely.

When Turnstile is composed, the magic-link and OTP send routes wear the humanity check automatically. You do not wire that.

Payments

RouteStrategy
POST /payments/purchasesuser
GET /payments/entitlementsuser
POST /payments/restoreuser
POST /payments/checkoutuser
POST /payments/portaluser
GET /payments/pricinguser
POST /payments/webhooks/applesigned-webhook
POST /payments/webhooks/googlesigned-webhook
POST /payments/webhooks/stripesigned-webhook
POST /payments/webhooks/lemon-squeezysigned-webhook
POST /payments/webhooks/paddlesigned-webhook
GET /payments/admin/catalogcontrol-plane
GET /payments/admin/purchasescontrol-plane
GET /payments/admin/subscriptionscontrol-plane
GET /payments/admin/entitlementscontrol-plane
GET /payments/admin/entitlements/:subjectType/:subjectIdcontrol-plane
GET /payments/admin/reconcile-runscontrol-plane
GET /payments/admin/discountscontrol-plane
POST /payments/admin/discountscontrol-plane
POST /payments/entitlements/grantcontrol-plane
POST /payments/entitlements/revokecontrol-plane

A grant is a control-plane call, not an admin user’s click. Comping somebody a year of Pro leaves a row signed by an issued credential with a scope on it, which is a different kind of record from a session that happened to belong to staff.

Storage

RouteStrategy
POST /storageuser — start an upload
GET /storageuser — list
POST /storage/:id/completeuser
POST /storage/:id/abortuser
GET /storage/:id/partsuser
POST /storage/:id/copyuser
POST /storage/:id/sharesuser
DELETE /storage/:id/shares/:tokenuser
GET /storage/:id/urluser — presign
PATCH /storage/:iduser
DELETE /storage/:iduser
HEAD /storage/:iduser
GET /storage/:idpublic, and the handler authorizes
GET /storage/share/:tokenpublic — the token is the credential

GET /storage/:id carries no requireAuth, on purpose: a public object has to be readable without a session, so authorization moves into the handler where it can see whether this object is public. The HEAD beside it does require a session — a metadata probe is not a download.

Media

RouteStrategy
POST /mediauser
GET /mediauser
POST /media/:id/finalizeuser
POST /media/duplicatesuser
GET /media/:iduser
DELETE /media/:iduser

Email

RouteStrategy
GET /email/jobscontrol-plane · email:jobs:read
GET /email/jobs/:idcontrol-plane · email:jobs:read
POST /email/jobs/:id/retrycontrol-plane · email:jobs:retry
GET /email/suppressionscontrol-plane · email:suppressions:read
POST /email/suppressionscontrol-plane · email:suppressions:write
POST /email/suppressions/removecontrol-plane · email:suppressions:delete
GET /_pithy/email/c/:tokenpublic — a click
GET /_pithy/email/o/:tokenpublic — an open
GET, POST /_pithy/email/u/:tokenpublic — unsubscribe

There is no end-user surface in the management file at all. A recipient interacts with email by receiving it; the three routes a recipient calls are the public callbacks, and each is gated by the signature on the token in its path.

Removing a suppression is a POST with a body, not DELETE /suppressions/:address. An address in a path is an address in every access log, proxy, trace and referrer between the client and the Worker.

Vector

RouteStrategy
POST /vector/:index/documentsuser
POST /vector/:index/queryuser
GET /vector/:index/documents/:iduser
DELETE /vector/:index/documents/:iduser

Secrets

RouteStrategy
GET /secrets/admin/statuscontrol-plane · secrets:status:read
GET /secrets/admin/status/:name/rotationscontrol-plane
POST /secrets/admin/status/:name/rotatecontrol-plane

Status, and never value. Nothing on this surface returns a secret; it returns whether one exists, when it was last rotated, and what happened on each rotation.

Audit

RouteStrategy
GET /audit/eventscontrol-plane
GET /audit/events/:eventIdcontrol-plane

Reads only. The trail is append-only, and this surface has no write route to omit — there is nothing to leave out, because there is nothing there.

Support

RouteStrategy
POST /support/feedbackuser — open a thread
GET /support/feedbackuser — my threads
GET /support/feedback/:iduser
GET /support/threadscontrol-plane
GET /support/threads/:idcontrol-plane
GET /support/repliescontrol-plane
POST /support/threads/:id/archivecontrol-plane
POST /support/threads/:id/replycontrol-plane
POST /support/threads/:id/reclassifycontrol-plane
POST /support/threads/:id/flagscontrol-plane

Testers

RouteStrategy
GET /testers/confirm/:tokenpublic — the invitation link
GET /testers/opt-in/:tokenpublic
GET, POST /testers/opt-out/:tokenpublic
GET /testers/statususer
GET /testers/cohortscontrol-plane
POST /testers/invitecontrol-plane
POST /testers/resendcontrol-plane
POST /testers/removecontrol-plane
POST /testers/nudgecontrol-plane

Opting out answers both verbs. A one-click unsubscribe header sends a POST; a person clicking the same link in a mail client sends a GET. Refusing either would refuse somebody trying to leave.

Ledger

RouteStrategy
GET /ledger/:currencyuser — my balance
GET /ledger/:currency/transactionsuser
POST /ledger/:currency/credituser + an admin scope
POST /ledger/:currency/debituser + an admin scope
GET /ledger/admin/accountscontrol-plane
GET /ledger/admin/accounts/:userIdcontrol-plane
GET /ledger/admin/accounts/:userId/:currency/transactionscontrol-plane

The two write routes stack a second guard on the first. A session alone reaches the balance; moving a balance also requires the configured admin scope on the caller. A user who could credit themselves is not a ledger.

Games

Leaderboard

RouteStrategy
GET /leaderboarduser
GET /leaderboard/:board/topuser
GET /leaderboard/:board/meuser
GET /leaderboard/:board/arounduser
POST /leaderboard/:board/segmentuser
PUT /leaderboard/:board/me/visibilityuser
POST /leaderboard/:boarduser + the submit scope, when server-authoritative
PUT /leaderboard/:board/entries/:userId/hiddenuser + admin scope
DELETE /leaderboard/:board/entries/:userIduser + admin scope

Rating

RouteStrategy
POST /rating/games/:game/outcomesuser + the record scope, when server-authoritative
GET /rating/games/:game/meuser
GET /rating/games/:game/players/:userIduser

Multiplayer

RouteStrategy
POST /multiplayer/games/:gameuser
POST /multiplayer/sessions/:id/joinuser
POST /multiplayer/sessions/:id/actionuser
POST /multiplayer/sessions/:id/leaveuser
POST /multiplayer/sessions/:id/closeuser
GET /multiplayer/sessions/:iduser
GET /multiplayer/sessions/:id/resultuser
GET /multiplayer/sessions/:id/socketuser — the WebSocket upgrade

Matchmaking

RouteStrategy
POST /matchmaking/games/:game/roomsuser
POST /matchmaking/rooms/:code/joinuser
POST /matchmaking/games/:game/invitesuser
GET /matchmaking/invitesuser
POST /matchmaking/invites/:id/acceptuser
POST /matchmaking/invites/:id/declineuser
GET /matchmaking/friendsuser
POST /matchmaking/friends/:userId/requestuser
POST /matchmaking/friends/:userId/acceptuser
POST /matchmaking/friends/:userId/declineuser
DELETE /matchmaking/friends/:userIduser
POST /matchmaking/games/:game/queueuser
GET /matchmaking/games/:game/queueuser
DELETE /matchmaking/games/:game/queueuser
GET /matchmaking/presenceuser

serverAuthoritative is the switch worth understanding before you ship a game. With it on, a score or an outcome is only accepted from a caller carrying the configured scope — which means from your own server, not from a client somebody can open in a debugger.

The control plane, and the kit’s own paths

RouteStrategy
GET /control-plane/pingcontrol-plane — any verified caller
GET /control-plane/manifestcontrol-plane · manifest read
GET /control-plane/keyscontrol-plane · keys rotate
POST /control-plane/keyscontrol-plane · keys rotate
POST /control-plane/keys/:keyId/expirecontrol-plane · keys rotate
GET /healthpublic — status and the deployed version
POST /__pithy/workflows/:bindingthe kit’s own durable-job dispatch
GET /__pithy/dev-loginlocal development only

/health is public because a health check that needs a credential is a health check nothing runs.

Two things this list implies

Your asset allowlist is derived from this table. run_worker_first has to name the paths the Worker owns, and every path above is one — which is why the CLI computes the list rather than asking you to keep one by hand.

A capability you have not composed mounts nothing. These are not reserved paths waiting to be claimed. Remove the capability and its prefix is free.

ESC