The grammar
Errors is why a payload carries a code rather than a sentence, English fallback is what a client renders when it has no words for one, and exit codes is the same contract at the CLI.
domain/reasonThe domain is the capability’s own name — the same segment that prefixes its tables and namespaces its migrations, which is what makes one capability’s codes incapable of colliding with another’s.
The code is the discriminator, and the taxonomy is closed. The kit’s codes are a union, each status is pinned to the one it maps to, and a mismatched pair fails validation rather than shipping.
Four fields, two of which cross the wire
| Field | Reaches a client | What it is |
|---|---|---|
code | Yes | Stable, and the thing to branch on |
message | Yes | The problem, in English, permanently |
params | Yes | Structured values a translating client interpolates |
action | No | The operator’s remedy. Stripped by the HTTP codec |
detail | No | Internal context for logs and audit. Stripped |
The code is already the translation key, so a locale file for the kit’s errors is exactly a file that covers these codes — and there is no second identifier to keep in sync.
What a status means to a client
| Status | Do |
|---|---|
| 400 | Fix the request. Retrying identically will not help |
| 401 | Mint a fresh access token. If that fails, sign in again |
| 403 | Do not retry. The caller is who they say and may not do this |
| 404 | It is not there |
| 409 | A conflict — usually a state that moved under you |
| 410 | Gone, permanently |
| 413 | Too large |
| 429 | Back off |
| 5xx | The deployment is at fault. Retry with backoff, and page somebody |
401 and 403 are the pair worth separating. A client that treats them the same loops: it refreshes a perfectly good token, gets another 403, and refreshes again.
Core, and the cross-cutting domains
| Code | Meaning |
|---|---|
validation/invalid_input | Input failed validation at a boundary. Carries the failing issues, so a form can highlight the field |
core/not_found | The requested resource does not exist |
core/conflict | The state moved under you |
core/internal | A fault in the deployment |
core/upstream_failed, core/upstream_timeout | A third party did not answer, or answered wrongly |
core/webhook_unverified | The sender’s proof did not check out |
core/workflow_failed, core/unknown_workflow, core/invalid_workflow_params, core/missing_workflow_binding | Durable-job faults. The last one is the first-run surprise — a Workflow binding to a host nothing has provisioned |
cloudflare/request_failed, cloudflare/invalid_response, cloudflare/not_configured | Outbound, from the CLI or CI. Never from a request handler |
Auth
| Code | Status | Meaning |
|---|---|---|
auth/invalid_token | 401 | The credential is missing, expired or invalid |
auth/forbidden | 403 | Authenticated, and not allowed to do this |
auth/provider_unavailable | 503 | An enabled social provider’s credential would not resolve. Every other sign-in method still works |
That last one is deliberately not a 404: a provider nobody enabled answers a 404, and nobody enabled this and this is broken right now are different facts that must never share an answer.
Payments
payments/invalid_receipt · payments/verification_failed · payments/webhook_unverified · payments/environment_mismatch · payments/receipt_already_owned · payments/product_not_found · payments/rail_not_configured · payments/provider_unavailable · payments/subject_unresolved · payments/entitlement_required · payments/entitlement_not_in_catalog · payments/discount_invalid · payments/clawback_failed
payments/environment_mismatch is the sandbox guard: a purchase whose store environment does not match this deployment’s is refused outright.
payments/entitlement_not_in_catalog is what a grant of an undeclared key gets — because a grant nothing declared is a grant nobody reviewed.
Storage
storage/not_found · storage/forbidden · storage/quota_exceeded · storage/upload_incomplete · storage/multipart_failed · storage/share_expired · storage/share_revoked
An object you may not see answers not-found rather than forbidden, because a 403 confirms the object exists.
email/template_not_found · email/invalid_payload · email/invalid_token · email/rate_limited · email/send_failed · email/suppressed
email/suppressed is not a failure to fix. The address is on the opt-out list, and that is the list working.
Secrets
secrets/not_found · secrets/already_exists · secrets/invalid_value · secrets/crypto_failed · secrets/rotation_unsupported · secrets/rotation_unrecorded
secrets/crypto_failed is what a name mismatch looks like — a ciphertext moved between rows does not open, which turns a query bug into a clean failure rather than a disclosure.
secrets/rotation_unrecorded exits 3, and it is the one failure the command cannot undo.
Control plane
controlplane/not_connected · controlplane/invalid_credential · controlplane/insufficient_scope · controlplane/key_not_found · controlplane/key_conflict
controlplane/not_connected is the shipped, denying state rather than a fault.
Turnstile
turnstile/missing_token · turnstile/failed · turnstile/config
A wrong secret is turnstile/config, a 500 — not a failed challenge. A 403 would send whoever is debugging it to look at the user, which is the wrong person and an hour gone.
Vector
vector/index_not_found · vector/unfilterable_field · vector/metadata_index_drift · vector/dimension_mismatch · vector/metadata_too_large · vector/filter_too_large · vector/topk_exceeded
vector/metadata_index_drift is the boot refusal — the config declares a filterable field the provisioning record does not have.
Ledger
ledger/insufficient_funds · ledger/invalid_amount · ledger/account_not_found · ledger/currency_not_found · ledger/hold_not_found · ledger/hold_not_open
ledger/insufficient_funds comes from a database constraint, not from a check in a handler — so it holds under concurrency.
Games
Multiplayer: multiplayer/session_not_found · multiplayer/game_not_found · multiplayer/not_a_member · multiplayer/session_full · multiplayer/invalid_move · multiplayer/invalid_transition
Matchmaking: matchmaking/room_not_found · matchmaking/room_full · matchmaking/invalid_code · matchmaking/invite_not_found · matchmaking/invite_forbidden · matchmaking/already_friends · matchmaking/friend_request_not_found · matchmaking/user_not_found · matchmaking/not_queued
Rating: rating/game_not_found · rating/pool_not_found · rating/record_forbidden · rating/unknown_algorithm · rating/unsupported_player_count · rating/invalid_params
Leaderboard: leaderboard/board_not_found · leaderboard/entry_not_found · leaderboard/board_immutable · leaderboard/invalid_schedule · leaderboard/score_rejected · leaderboard/submit_forbidden
rating/unsupported_player_count fails on deploy rather than at the first result — a 1v1 algorithm wired to a four-player game is caught before it produces a month of nonsense.
Media, support, testers, audit
Media: media/not_found · media/unsupported · media/storage_failed · media/enrichment_failed
Support: support/not_found · support/invalid_category · support/rejected · support/unparseable_message · support/classification_failed · support/reply_failed
Testers: testers/not_configured · testers/cohort_not_found · testers/cohort_closed · testers/member_not_found · testers/already_on_roster · testers/withdrawn · testers/roster_full · testers/invalid_token · testers/nudge_cooldown · testers/copy_not_allowed
Audit: audit/invalid_event · audit/write_failed
audit/write_failed never fails the audited action. It is logged and the action proceeds — an audit trail that can take your login down is worse than one with a gap in it.
Where this list comes from
The closed union in @pithy-sh/core’s error payload, plus each capability’s own error module. Those are the authority; this page is a hand-maintained rendering of them.