Skip to content

Errors

Every non-2xx response carries a flat envelope:

json
{
  "code": "invalid_request",
  "message": "email is required",
  "fields": { "email": "must be a valid email address" },
  "requestId": "req_9f7a32b5"
}
  • code — machine-readable. Stable. Branch on this.
  • message — human-readable. May change. Don't string-match.
  • fields — present on validation errors. Maps dotted field paths to per-field messages.
  • requestId — server-generated. Include this when reporting an issue.

HTTP status conventions

StatusWhen
200Success with a body.
201Resource created.
202Approval-gated mutation diverted to a proposal — see Location header.
204Success, no body.
303Magic-link redirect.
304If-None-Match matched; resource unchanged.
400Bad request — code: invalid_request (with fields) or a more specific code.
401Not authenticated.
403Authenticated but not authorized.
404Not found, or cross-tenant access (collapsed from 403 to avoid leaking URL existence).
409Conflict — see code-specific table below.
410Gone — terminal state on a one-shot resource (invitation, proposal, magic-link).
412If-Match precondition failed.
429Rate limit exceeded — see Retry-After.
500Server error — code: internal.
503Temporary unavailability — most commonly ruleset_not_cached on a fresh env.

Cross-tenant access (e.g. asking for an environment in an org you're not a member of) always collapses to 404, never 403. URL existence shouldn't leak.

Code catalog

General

CodeStatusWhen
invalid_request400Bad shape, missing required field, type mismatch, invalid enum. fields carries the per-field messages.
unauthorized401No / invalid credentials.
forbidden403Authenticated but not authorized for this resource.
not_found404Resource doesn't exist (or cross-tenant access).
precondition_failed412If-Match ETag didn't match the current row.
internal500Server error. The response includes a requestId — bring it when reporting.
ruleset_not_cached503Environment has no materialized ruleset yet (only happens on a brand-new env that hasn't been written to). Retry after the first write.

Authentication

CodeStatusWhen
magic_link_invalidredirectToken unknown or malformed.
magic_link_usedredirectToken already redeemed.
magic_link_expiredredirectToken past its 15-minute TTL.
no_invitationredirectMagic-link sign-in attempted for an unknown email on a hosted instance with no pending invitation.
already_claimed409POST /auth/install-claim against an instance that already has at least one org.
slug_unavailable409Org slug already in use (signup or org create).

Authorization

CodeStatusWhen
insufficient_role403Session caller's org role is below the endpoint's minimum.
scope_denied403Bearer token's scope doesn't admit the requested env / resource / action.
token_action_denied403Bearer token's actions list doesn't include the required verb.
token_env_out_of_scope403Bearer token's environments scope doesn't include this env.
token_resource_out_of_scope403Bearer token's resources scope doesn't match this primitive's key.
token_ip_denied403Bearer token has an IP allowlist and the source IP isn't on it.
token_rate_limit429Per-token bucket exhausted. Retry-After carries seconds.
approval_not_supported403Approval-required token attempted a mutation that can't be diverted to a proposal (e.g. metadata patch, mixed-dimension state replace).

Members

CodeStatusWhen
last_owner_cannot_demote_or_remove409Demoting or removing the last active owner. Promote someone else to owner first.
already_member409Inviting an email that's already an org member.
invitation_pending409A still-fresh pending invitation exists for the same (org, email).

Invitations

CodeStatusWhen
invitation_used410Invitation already accepted.
invitation_expired410Invitation past its 7-day TTL.
invitation_email_locked410The email matches a soft-deleted user. Operator must hard-delete to free the email.

Segments

CodeStatusWhen
in_use409Deleting a segment that's referenced from another flag, config, or segment. fields lists referrers as <resourceType>.<resourceKey>.

Proposals

CodeStatusWhen
version_drift409Apply attempted after the env's version moved. Body carries {liveVersion, proposedVersion}; re-propose against the new state.
proposal_gone410Apply or cancel attempted on a proposal that's already applied, cancelled, or expired.

Confirmation

CodeStatusWhen
invalid_confirmation400A destructive endpoint (e.g. DELETE /orgs/{slug}) requires the caller to type the resource's slug as a confirmation token; mismatched or missing returns this. fields.confirm is populated.

Rate-limit headers

Every authenticated bearer response with status 2xx, 304, or 429 carries:

  • X-RateLimit-Limit — requests per minute cap.
  • X-RateLimit-Remaining — requests remaining in the current bucket.
  • X-RateLimit-Reset — integer seconds until the bucket fully refills.

On 429, also Retry-After: <seconds> — the load-bearing "when can I retry" signal.

Two buckets gate every request: per-token (the key's rateLimitPerMin) and per-org aggregate. Headers reflect the more restrictive bucket; on 429, the bucket that denied.

Session-authenticated responses don't carry rate-limit headers.