Error Codes
AnvilBase uses standard HTTP status codes with structured error bodies. This page maps each code to its meaning and the usual cause.
Error envelopes
Management plane (/api/v1):
{ "error": { "message": "human-readable description", "code": 404 } }Data plane (/v1):
{ "error": "invalid query", "statusCode": 400, "hint": "...", "details": "..." }Internal/database details are never leaked — a server-side failure always returns a generic message; the specifics are in the server logs.
Status codes
| Code | Meaning | Typical cause |
|---|---|---|
200 | OK | successful read/update |
201 | Created | resource created (project, table, secret, user, function…) |
202 | Accepted | async job queued (e.g. PITR restore) |
204 | No Content | successful delete / no-body success |
400 | Bad Request | invalid input, failed validation, bad ?confirm= echo, malformed query |
401 | Unauthorized | missing/invalid/expired credential (management plane fails closed) |
403 | Forbidden | reserved (e.g. suspended project) |
404 | Not Found | resource doesn’t exist or cross-tenant access denied (IDOR-safe) |
409 | Conflict | duplicate slug/name/email or unique-constraint violation |
429 | Too Many Requests | rate limit, per-project DB pool exhaustion, or quota exceeded |
500 | Internal Server Error | server-side failure (details logged, not returned) |
503 | Service Unavailable | upstream timeout (e.g. auth service) |
Notable specifics
401 (management)
Messages: missing authorization header, invalid platform credential,
expired platform token. You’re using the wrong credential type (e.g. a
service_role key on /api/v1) or no token. Use the admin token or a PAT — see
Management API → Auth.
404 as access control
Cross-tenant access (guessing another project’s UUID) returns 404, not 403 — the
resource’s existence isn’t revealed. This is intentional (IDOR-safe). See
Multi-Tenancy.
409 (conflict)
- Project slug already exists.
- Pending invite already exists for that email on the project.
- Function name already exists on create.
- A unique/foreign-key constraint violation on a write.
429 (three distinct causes)
| Sub-cause | Signal |
|---|---|
| Rate limit | X-RateLimit-Remaining: 0 + Retry-After |
| DB pool exhausted | {"error":{"kind":"db_pool_exhausted","code":429}} + Retry-After: 1 |
| Quota exceeded | on storage writes / function deploys over the project limit |
All three are back-pressure — retry with exponential backoff. See Scaling and Rate limits.
503 (upstream timeout)
{ "error": "upstream_timeout", "service": "auth" }A backing service (commonly auth) didn’t respond in time. If /health/services
shows that service unhealthy, scale it, not the control plane
(Scaling).
Handling errors well
- Retry
429and503with exponential backoff and jitter; respectRetry-After. - Don’t retry
400/401/403/404/409— fix the request. - Surface
hint/details(data plane) in logs — they usually pinpoint the problem (a constraint, a bad column, an RLS rejection). - A write that silently returns zero rows under the
anonkey is usually RLS (no policy permits it), not an error — see Row Level Security.
Next: Glossary.