Errors
The envelope
Section titled “The envelope”Every JSON error uses one shape:
{ "error": { "code": "VALIDATION", "message": "title is required" } }Always content-type: application/json.
Domain errors
Section titled “Domain errors”| Thrown internally | Status | code |
|---|---|---|
ValidationError |
400 | VALIDATION |
ForbiddenError |
403 | FORBIDDEN |
NotFoundError |
404 | NOT_FOUND |
ConflictError |
409 | CONFLICT |
RateLimitError |
429 | RATE_LIMITED |
UpstreamError |
502 | UPSTREAM_FAILED |
VectorSearchUnavailableError |
409 | VECTOR_SEARCH_UNAVAILABLE |
| anything else | 500 | INTERNAL, message fixed to internal server error |
A 500 never leaks details — the real error is in the server log, not the response.
Auth codes
Section titled “Auth codes”| Code | Status | Meaning |
|---|---|---|
EMAIL_TAKEN |
409 | Registration with an address that already exists. |
WEAK_PASSWORD |
400 | Below the minimum length. |
INVALID_CREDENTIALS |
401 | Wrong email or password. Deliberately identical for both. |
USER_NOT_ACTIVE |
403 | The account is disabled or blocked. |
EMAIL_NOT_VERIFIED |
403 | Verification required and not done. |
INVALID_SESSION |
401 | The refresh token is unknown or already used. |
INVALID_TOKEN |
401 | Malformed, expired, or wrong audience. |
Transport codes
Section titled “Transport codes”| Code | Status | Usual cause |
|---|---|---|
MISSING_TOKEN |
401 | No Authorization header on a route that needs one. |
INVALID_TOKEN |
401 | Bad or expired token, or the wrong audience for the route. |
INSUFFICIENT_SCOPE |
403 | The API key lacks the scope this route needs. |
PROJECT_MISMATCH |
400 | An x-groveback-project header disagreeing with the token’s pid. (403 on the per-project spec route.) |
MISSING_PROJECT |
400 | Multi-project deployment, no project named. |
INVALID_BODY |
400 | Unparseable JSON, or $ keys in a PUT. |
INVALID_FILTER |
400 | The filter param is not a JSON object. |
INVALID_PARAM |
400 | A negative or non-integer limit/skip. |
INVALID_REDIRECT |
400 | An OAuth redirect origin that is not allow-listed. |
INVALID_STATE |
400 | The OAuth state expired (10 min) or was tampered with. |
INVALID_SIGNATURE |
400 | A bad signed-URL or webhook signature. |
RATE_LIMITED |
429 | Comes with a retry-after header. |
BILLING_DISABLED |
400 | A billing route on a deployment without Stripe. |
METHOD_NOT_ALLOWED |
405 | Right path, wrong verb. |
Realtime codes
Section titled “Realtime codes”Delivered as { "type": "error", "code": … } frames, not HTTP:
INVALID_MESSAGE · AUTH_FAILED · UNAUTHENTICATED · DUPLICATE_ID ·
UNKNOWN_SUBSCRIPTION · NOT_FOUND · FORBIDDEN · TOKEN_EXPIRED · CONNECTION_LIMIT
Two things that are not JSON errors
Section titled “Two things that are not JSON errors”HTML pages. /auth/login, /reset-password, and OAuth start failures when the client
accepts text/html render an error page instead of the envelope. API clients still get JSON.
The OAuth callback never returns an error status. It 302s back to your app with
#mb_error=<reason> — provider_denied, exchange_failed, or a lowercased auth code.
Reading a 404
Section titled “Reading a 404”404 is used for three distinct situations, on purpose:
- The thing does not exist.
- It exists but you may not see it — a policy filter excluded it, or you are not a project member.
- The feature is not wired on this deployment — storage, functions, audit, billing, introspection and the whole control plane all 404 when absent.
A 403 in case 2 would confirm the row exists, so the API declines to distinguish.
Plan-limit errors
Section titled “Plan-limit errors”Every denial caused by a plan limit has a message starting with plan limit: , whether it
arrives as a 403, a 429, a run-log entry or a WebSocket error. That prefix is the reliable way
to detect a quota problem across surfaces.
Function endpoint statuses
Section titled “Function endpoint statuses”| Situation | Status |
|---|---|
| Handler threw | 500 { "error": "function error" } — the message is in the run log, never in the response. |
| Timed out | 504 { "error": "function timed out" } |
| Worker pool saturated | 503 — retryable. |
| Pre-hook aborted a write | 400 VALIDATION, with the hook’s reason as the message. |