https://api.gavai.app/v1 returns the same JSON envelope and a matching HTTP status code. The status tells you the problem class; the error.code tells you the specific reason. By the end of this page you will know which codes are safe to retry, what details carries for validation failures, and how to read scope errors.
The envelope
400) extend the envelope with a fields array, one entry per offending field:
Status codes
Common error codes
Inspecting details by error class
The details object varies by error.code. The shapes worth knowing because handlers branch on them:
Validation failures
400 validation_failed carries fields[] — one entry per offending field. Each entry has a JSON-pointer-style path and a reason that is one of a small enumerated set. Branch on reason if your client needs to render structured form errors:
missing, too_short, too_long, format_mismatch, out_of_range, unknown_value, and unknown_field. Treat anything you don’t recognize as a generic validation error.
Conflict kinds
409 conflict carries details.kind so you can tell what kind of conflict happened. Each kind has a different recovery path:
Auth hints
401 unauthorized and 403 insufficient_scope carry a details.hint that names the specific recovery action. The hint is enumerated so a client can branch on it without parsing the message:
Upstream failures
When a capability dispatch fails because the downstream provider is unavailable — Stripe is rate-limiting, the email gateway returned 5xx, the database connection pool is saturated — the error envelope usescode: "upstream_unavailable" with details.upstream naming the failing service and details.retryable set to true when the dispatcher believes a retry will succeed:
upstream is enumerated: payments, email, storage, database, identity, dns. Use it to surface a targeted message to your user rather than a generic “something went wrong.”
Policy denials
A request that hit a policy rule rather than a scope check returns403 policy_denied with a structured diagnostic naming the rule that fired. The diagnostic is intentionally minimal — it tells you that a rule blocked the request without revealing the rule’s predicate:
rule_id is stable per rule; the console exposes a human-readable description for each. Display it to operators rather than end users.
Retrying safely
Never retry a 4xx without changing the request — the outcome will not change. For 5xx and 429:- Respect
Retry-Afteron 429 before attempting again - Apply exponential backoff with jitter on 5xx — start at 1 second, double each attempt, cap at a sensible ceiling
- Attach an
Idempotency-Keyheader on writes so retries do not produce duplicate side effects (see Idempotency)
Next
Idempotency
How
Idempotency-Key makes write retries safe across a 24-hour window.