Skip to content

Plat5 API Error Response Standard

Standard envelope across Plat5 services.

Clients show error.message. Branch on code and HTTP status. Do not parse details to invent copy.

Product sentences live in error-copy.md.

{
"error": {
"type": "invalid_request_error",
"code": "VALIDATION_ERROR",
"message": "Name is required.",
"request_id": "abc-123",
"details": {
"fields": [
{ "path": "name", "message": "Name is required." }
]
}
}
}
Field Type Description
type string invalid_request_error, api_error
code string Machine-readable identifier (UPPER_SNAKE_CASE)
message string Human-readable sentence. Safe to show in a UI.
request_id string Correlation ID from X-Request-ID. Propagate it; do not generate it.
details object | null Type-specific context. Shape varies by code. Never the only place the human sentence lives.

request_id is also returned in the X-Request-ID response header on every public API response. The gateway handles this; services behind the gateway must not set it.

Not required on: CORS preflight (OPTIONS) and internal health checks (/health/live, /health/ready).

The Fallback message column is used only when nothing more specific applies. UNAUTHORIZED, NOT_FOUND, INTERNAL_ERROR, and SERVICE_UNAVAILABLE stay generic on purpose.

Code HTTP Type Fallback message Details
INVALID_REQUEST 400 invalid_request_error Malformed request. —
VALIDATION_ERROR 422 invalid_request_error That doesn’t look right. { fields: [{ path, message }] }
UNAUTHORIZED 401 invalid_request_error Authentication required. { reason }
FORBIDDEN 403 invalid_request_error You don’t have permission to do that. { permission, resource, resource_id, required_labels }
NOT_FOUND 404 invalid_request_error Resource not found. { resource, id }
CONFLICT 409 invalid_request_error That already exists. { field, value }
ORGANIZATION_HANDOFF_REQUIRED 409 invalid_request_error Hand off or delete your organizations first. { organizations: [{ id, name }] }
ROUTE_CONFLICT 409 invalid_request_error (route-registry apply, put, restore) Message lists each conflicting route (same method, same path shape) and its owner service. —
PAYLOAD_TOO_LARGE 413 invalid_request_error Request body is too large. { max_size_bytes }
RATE_LIMITED 429 api_error Too many requests. Try again in a moment. { retry_after_seconds }
INTERNAL_ERROR 500 api_error An unexpected error occurred. null
SERVICE_UNAVAILABLE 503 api_error Service temporarily unavailable. null

Returned by the gateway when the member’s role labels share no label with the route’s required_labels. details.required_labels is the route’s list. Any one of those labels would have admitted the request.

Returned by identity on account deletion while the person is the last member, or the last creator_role holder, of an org. details.organizations lists those orgs. The person hands them to someone else, or deletes them, first (identity.md).

Returned by the gateway, not downstream services. See gateway-contract.md and identity-boundary.md.

Canonical policy: identity-boundary.md.

Wrong credential for the scope, or an invalid key or session, is UNAUTHORIZED (401). Credential validate unavailable → SERVICE_UNAVAILABLE (503). A path param that is not one segment is INVALID_REQUEST (400). The gateway does not 404 a wrong credential.

Returned by the gateway. HTTP 429. Retry-After (seconds) is set to the same value as details.retry_after_seconds.

Two independent Valkey limiters (replicas share one budget). VALKEY_URL is required to boot. Valkey error or timeout → 503 SERVICE_UNAVAILABLE. The gateway opens a new connection when Valkey answers again. Named policies and buckets: routes.md. Admitted limited routes set X-RateLimit-Limit / Remaining / Reset on 2xx and 429. Failed-auth limiter sets Retry-After only.

Limiter When Key
Admitted route After match + admission (JWT, API key, and session). Omitted rate_limit inherits RATE_LIMIT_REQUESTS / RATE_LIMIT_WINDOW_SECONDS. false opts out. Inline object = this route+method. Policy name = service rate_limits entry (shared: true is opt-in cross-service). Subject from route scope (public→ip, user→user, organization→org, member→member), plus method+path or policy name.
Failed-auth IP Unadmitted 401s and unmatched 404s. Not per-route. Client IP. RATE_LIMIT_AUTH_FAILURE_REQUESTS / RATE_LIMIT_AUTH_FAILURE_WINDOW_SECONDS (default 60/60). 0 requests = off
  • Consistent envelope — Every error has the same top-level error object.
  • message is for humans — One sentence. No regex, no code names, no bind-error junk.
  • Never leak internals — INTERNAL_ERROR responses do not include stack traces. Log those server-side.
  • Gateway handles authn — Gateway returns UNAUTHORIZED. Downstream services must not.