🛡️Gatekeeper/ SDKs

When a request fails, the API returns the error envelope and the SDK throws. This page is the complete, source-traced map of error codes, statuses, and triggers from apps/api/src/error-map.ts and the per-package error constants.

The error envelope#

A failed request returns:

{ "ok": false, "error": "Invalid email or password", "code": "unauthorized" }

The root app.notFound (unknown path) and app.onError (unhandled exception) return { ok: false, error } without a code at 404 / 500 respectively.

How the SDK throws#

The SDK core throws a GatekeeperError on any ok !== true response or non-2xx status. Its shape:

FieldSourceNotes
statusHTTP statuse.g. 401, 429
messagethe envelope error stringhuman-readable
codethe envelope codestable machine code; may be absent on raw 404/500
retryAfterthe Retry-After headerparsed on 429s; seconds

In TypeScript it is an Error subclass (instanceof GatekeeperError); in Python it is an Exception subclass. Catch it and branch on code or status.

Status to code#

fail() derives the default code from the status via codeForStatus() (STATUS_TO_CODE in error-map.ts):

StatusCodeClass
400bad_requestclient error / validation
401unauthorizedbad or missing credential
402plan_limitentitlement / quota cap reached
403forbiddenauthenticated but not permitted
404not_foundresource absent or hidden
409conflictstate conflict
413payload_too_largebody over the limit
429rate_limitedrate-limit tier exceeded
503service_unavailabledependency fault, fail-closed

Status class reference#

400 bad_request#

The default for any error string not explicitly mapped, plus validation failures. Services convert unexpected thrown exceptions into a generic string, which also falls through to 400.

401 unauthorized#

A credential was presented and it was bad (invalid, revoked, expired), or required and absent.

Error stringSource constant
Invalid email or passwordERROR_MESSAGES.AUTH.INVALID_CREDENTIALS
Invalid refresh tokenERROR_MESSAGES.AUTH.INVALID_REFRESH_TOKEN
Refresh token expiredERROR_MESSAGES.AUTH.REFRESH_TOKEN_EXPIRED
Invalid API keyAPI_KEY_ERRORS.INVALID_KEY
API key has been revokedAPI_KEY_ERRORS.REVOKED
API key has expiredAPI_KEY_ERRORS.EXPIRED
Invalid payment webhook signaturePAYMENT_ERRORS.INVALID_WEBHOOK_SIGNATURE
Payment webhook context does not match provider configPAYMENT_ERRORS.WEBHOOK_CONTEXT_MISMATCH
Stale payment webhookPAYMENT_ERRORS.STALE_WEBHOOK
invalid platform service account keyPLATFORM_SERVICE_ACCOUNT_ERRORS.INVALID_KEY
platform service account revokedPLATFORM_SERVICE_ACCOUNT_ERRORS.REVOKED
platform service account expiredPLATFORM_SERVICE_ACCOUNT_ERRORS.EXPIRED

The decision engine also emits 401 directly for UNAUTHENTICATED (no credential where one is required) and INVALID_CREDENTIAL (presented credential failed to resolve).

402 plan_limit#

An entitlement cap was reached. Route handlers set this explicitly when an entitlement check fails (team members, API keys, or webhooks limit reached). The decision engine maps ENTITLEMENT_EXCEEDED and QUOTA_EXCEEDED to 402.

403 forbidden#

The caller is authenticated but not permitted. The message is almost always the literal "forbidden".

Error stringSource constant
forbidden (generic guard rejection)error-map.ts literal
platform permission deniedPLATFORM_SERVICE_ACCOUNT_ERRORS.FORBIDDEN

Engine deny reasons that resolve to 403: NOT_A_MEMBER, INSUFFICIENT_ROLE, PERMISSION_DENIED, PLATFORM_PERMISSION_DENIED. Routes that minted a key token from a scopeless key also return 403 explicitly. Note that a NOT_A_MEMBER 403 is remapped to 404 on routes with hideNotFound.

404 not_found#

A resource is absent, or hidden to avoid leaking its existence to non-members.

Error stringSource constant
User not found or suspendedERROR_MESSAGES.USER.NOT_FOUND
API key not foundAPI_KEY_ERRORS.NOT_FOUND
Tenant not foundQUOTA_ERRORS.TENANT_NOT_FOUND
Role not foundPERMISSIONS_ERRORS.ROLE_NOT_FOUND
Webhook endpoint not foundWEBHOOK_ERRORS.ENDPOINT_NOT_FOUND
Payment provider config not foundPAYMENT_ERRORS.PROVIDER_CONFIG_NOT_FOUND
Payment plan mapping not foundPAYMENT_ERRORS.PLAN_MAPPING_NOT_FOUND
Payment customer not foundPAYMENT_ERRORS.CUSTOMER_NOT_FOUND
Unknown planPAYMENT_ERRORS.UNKNOWN_PLAN
job not foundJOB_ERRORS.NOT_FOUND
platform service account not foundPLATFORM_SERVICE_ACCOUNT_ERRORS.NOT_FOUND

A hideNotFound route additionally turns a NOT_A_MEMBER 403 into this 404.

409 conflict#

A state conflict - the request is well-formed but collides with existing state.

Error stringSource constant
Email already registeredERROR_MESSAGES.AUTH.EMAIL_ALREADY_REGISTERED
Email already verifiedERROR_MESSAGES.AUTH.EMAIL_ALREADY_VERIFIED
Email verification link has already been usedERROR_MESSAGES.AUTH.VERIFICATION_TOKEN_ALREADY_USED
Reset link has already been usedERROR_MESSAGES.AUTH.PASSWORD_RESET_TOKEN_ALREADY_USED
Magic link has already been usedERROR_MESSAGES.AUTH.MAGIC_LINK_ALREADY_USED
MFA is already enabled. Disable first to re-enrollERROR_MESSAGES.AUTH.MFA_ALREADY_ENABLED
User is already a member of this tenantERROR_MESSAGES.AUTH.ALREADY_MEMBER
Idempotency key already used for a different usage eventQUOTA_ERRORS.IDEMPOTENCY_CONFLICT
Role name already existsPERMISSIONS_ERRORS.DUPLICATE_ROLE_NAME
Role is in usePERMISSIONS_ERRORS.ROLE_IN_USE
Payment webhook event payload changed for the same event idPAYMENT_ERRORS.WEBHOOK_PAYLOAD_MISMATCH
Payment provider config is disabledPAYMENT_ERRORS.PROVIDER_CONFIG_DISABLED
Payment provider operation is unsupportedPAYMENT_ERRORS.UNSUPPORTED_OPERATION

413 payload_too_large#

The request body exceeds the limit. In practice the payment webhook ingest route caps the body at 256 KiB.

429 rate_limited#

A rate-limit tier was exceeded. The response carries a Retry-After header (seconds), which the SDK parses into GatekeeperError.retryAfter. The engine emits this for the RATE_LIMITED deny reason.

503 service_unavailable#

A dependency faulted and the request was rejected fail-closed. This is never a verdict about the caller; it is retryable.

Error stringSource constant
Payment credential key must be 32 random bytesPAYMENT_ERRORS.INVALID_CREDENTIAL_KEY
Payment store operation failedPAYMENT_ERRORS.STORE_ERROR
Payment provider request failedPAYMENT_ERRORS.PROVIDER_ERROR
job store errorJOB_ERRORS.STORE_ERROR
platform service account store errorPLATFORM_SERVICE_ACCOUNT_ERRORS.STORE_ERROR

The decision engine maps every operational fault reason (identity, membership, permission, entitlement, quota, or rate-limiter backend unavailable, plus an internal try/catch) to 503. A backend fault never silently becomes an allow.

Mapping mechanics#

Domain error strings are exact-matched in ERROR_STATUS (error-map.ts); anything unlisted defaults to 400. Because the map keys off each package's exported error constant, renaming a constant is a compile error here rather than a silent status change.