Error codes
The error body
Section titled “The error body”Errors are JSON. Most carry a typed code you can switch on:
{ "error": "Unauthorized", "code": "TOKEN_EXPIRED", "message": "Token has expired. Refresh via /auth/refresh or re-run `vaif login`.", "requestId": "req_…"}error is the HTTP status class, code the specific reason, and details, when present, has field-level information. Some routes answer with an upper-case error and no code, such as { "error": "NOT_FOUND", "message": "…" }, and plan limits name the limit in message. Quote the requestId when you contact support.
Status codes
Section titled “Status codes”| Status | Meaning |
|---|---|
| 400 | The request is malformed or failed validation |
| 401 | Missing, invalid or expired credentials |
| 402 | A plan quota is used up |
| 403 | Signed in, but not allowed: role, scope, plan limit or a deactivated project |
| 404 | Not found, or not visible to you |
| 409 | Conflicts with something that exists |
| 429 | Rate limited: wait for Retry-After seconds |
| 500 | Server error: retry, and quote the requestId if it persists |
Typed codes
Section titled “Typed codes”This table is the API’s own list of typed codes, with the message each one answers with.
| Code | Status | Message |
|---|---|---|
TOKEN_MISSING | 401 Unauthorized | Missing Authorization header. Send 'Authorization: Bearer <jwt>' or 'x-vaif-key: vk_<...>'. |
TOKEN_INVALID | 401 Unauthorized | Token signature is invalid or the JWT is malformed. |
TOKEN_EXPIRED | 401 Unauthorized | Token has expired. Refresh via /auth/refresh or re-run vaif login. |
SCOPE_INSUFFICIENT | 403 Forbidden | Your token does not grant access to this endpoint. Use a user JWT for control-plane routes. |
CORS_ORIGIN_NOT_ALLOWED | 403 Forbidden | This browser origin is not allowed. Change allowed origins in Project settings → API. |
INVALID_API_KEY | 401 Unauthorized | API key not recognized. Check the value in the dashboard. |
PROJECT_NOT_FOUND | 404 Not found | Project does not exist or you don't have access. |
FEATURE_DISABLED | 403 Forbidden | This feature is off for this project. Turn it on in Project settings → Features. |
PROJECT_DEACTIVATED | 403 Forbidden | This project has been deactivated. Contact support@vaif.studio to reactivate. |
SESSION_NOT_FOUND | 404 Not found | Session not found or already revoked. |
INVALID_WHERE | 400 Bad request | Invalid where clause shape. Expected {field: string, operator: 'eq'|'neq'|..., value: any}. |
INVALID_SELECT | 400 Bad request | Invalid select. Use comma-separated column names or '*'. |
INVALID_COLUMN | 400 Bad request | Column does not exist on this table. |
LIMIT_TOO_HIGH | 400 Bad request | limit must be ≤ 1000. Use offset for pagination. |
SQL_PARSE_ERROR | 400 Bad request | SQL is not valid Postgres syntax. |
SQL_FORBIDDEN_SET_ROLE | 400 Bad request | SET ROLE is not allowed; the engine manages role per project. |
SQL_FORBIDDEN_SET_SESSION_AUTH | 400 Bad request | SET SESSION AUTHORIZATION is not allowed. |
SQL_FORBIDDEN_SET_SEARCH_PATH | 400 Bad request | SET search_path is not allowed; the engine sets it per project. |
SQL_FORBIDDEN_RESET_ROLE | 400 Bad request | RESET ROLE is not allowed. |
SQL_FORBIDDEN_EXTENSION | 400 Bad request | CREATE/DROP EXTENSION is not allowed. |
SQL_FORBIDDEN_SECURITY_DEFINER | 400 Bad request | SECURITY DEFINER functions are not allowed. |
SQL_FORBIDDEN_COPY_PROGRAM | 400 Bad request | COPY FROM PROGRAM is not allowed. |
SQL_FORBIDDEN_CROSS_SCHEMA | 400 Bad request | Statements may only reference your project's schema, pg_catalog, or information_schema. |
SQL_FORBIDDEN_TEMP_SCHEMA | 400 Bad request | Temporary tables, views, sequences and types are not allowed, and statements may not reference the pg_temp or pg_toast schemas. |
SQL_FORBIDDEN_TRANSACTION_CONTROL | 400 Bad request | Transaction control is not allowed; each request already runs as one transaction (a single leading BEGIN and trailing COMMIT are accepted). |
SQL_FORBIDDEN_SET_CONFIG | 400 Bad request | set_config() may not change role, session_authorization, search_path or the read-only settings, and must be transaction-local (third argument true). |
SQL_FORBIDDEN_SESSION_SET | 400 Bad request | Session-level SET and RESET are not allowed; use SET LOCAL, which lasts for the request's transaction. |
SQL_FORBIDDEN_SET_TIMEOUT | 400 Bad request | Use SET LOCAL with a positive literal timeout no higher than the platform cap. Zero, session SET and dynamic timeout changes are refused. |
SQL_FORBIDDEN_SETTING | 400 Bad request | This setting cannot be changed from SQL: memory, parallel-worker, JIT and other server resource settings are set by the platform. Formatting, planner switches and namespaced settings (request.*) may be set with SET LOCAL or set_config(..., true). |
SQL_FORBIDDEN_SETTINGS_WRITE | 400 Bad request | Writing pg_settings is not allowed; it changes server settings for the whole session. |
SQL_FORBIDDEN_STATEMENT | 400 Bad request | This kind of statement is not allowed (code blocks, functions, roles, grants, prepared statements, NOTIFY, and server-level commands). |
SQL_FORBIDDEN_FUNCTION | 400 Bad request | This function is not allowed (it runs SQL text, reads server files, uses large objects or advisory locks, or signals other sessions). |
SQL_FORBIDDEN_COPY_FILE | 400 Bad request | COPY to or from a server file is not allowed. |
MULTI_STATEMENT_SQL_RESULT | 200 | Multi-statement SQL — response is an array of per-statement results. |
RATE_LIMITED | 429 Too many requests | Too many requests. Slow down. |
QUOTA_EXCEEDED | 402 Payment required | Plan quota exceeded. Upgrade at https://console.vaif.studio/app/billing. |
ORIGIN_NOT_ALLOWED | 403 Forbidden | Origin not in the project's allowed list. |
SECRET_KEY_IN_BROWSER | 403 Forbidden | This is a secret key (vk_srv_…), and secret keys only work from a server. In browser code use the project's publishable key (vk_pub_…) and row rules; keep the secret key on your server. |
CSRF_TOKEN_INVALID | 403 Forbidden | Missing or invalid CSRF token. Reload the page and try again. |
REAUTH_REQUIRED | 403 Forbidden | Confirm it's you before changing how you sign in. |
MFA_LOCKED | 429 Too many requests | Too many failed MFA attempts. Try again in a few minutes. |
IMPERSONATION_READ_ONLY | 403 Forbidden | This is a read-only impersonation session — writes as the impersonated user are blocked. |
MCP_DISABLED | 403 Forbidden | Hosted MCP is not enabled on this server. |
MCP_CONNECTION_ENDED | 401 Unauthorized | This MCP connection was revoked or no longer exists. Reconnect from your MCP client. |
MCP_PROJECT_MISMATCH | 403 Forbidden | This MCP connection is bound to a different project. |
MCP_READ_ONLY | 403 Forbidden | This MCP connection is read-only. Reconnect and allow writes to change data. |
MCP_ROUTE_NOT_ALLOWED | 403 Forbidden | This endpoint is not available to hosted MCP connections. |
MCP_PRODUCTION_WRITES_DISABLED | 403 Forbidden | Production writes are not allowed on this MCP connection. Reconnect and allow production writes (admin role). |
BAD_REQUEST | 400 Bad request | Request validation failed. |
INTERNAL_ERROR | 500 Server error | Unexpected server error. Reference the requestId when contacting support. |
Rate limits
Section titled “Rate limits”Requests your app makes with an API key are limited per key and per signed-in user (per client address when nobody is signed in), so one busy user cannot use up the key for everyone else. Each key also has an overall ceiling. The routes for your app’s users have their own limits per client address and project, for example 5 sign-ups per 10 minutes and 30 refreshes per minute.
A limited request gets 429 with a Retry-After header, in seconds. Browsers can read it, so a web app can wait exactly that long; @vaifstudio/client gives it to you as VaifRateLimitError.retryAfter. Responses also carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
Debugging
Section titled “Debugging”- Read the whole body.
codeandmessageusually say exactly what is wrong. - Check the key. A
vk_pub_…key sees only the rows the row rules allow, and a request without the user’s access token is anonymous. - Check the function logs on the Functions page when a function fails.
- Watch the rate-limit headers before you reach a
429.
Errors in the SDK
Section titled “Errors in the SDK”@vaifstudio/client throws typed errors. Every one extends VaifError, which has code, statusCode, requestId and details:
import { createVaifClient, VaifError, VaifRateLimitError, type VaifErrorCode } from "@vaifstudio/client";
const vaif = createVaifClient({ projectId: process.env.VAIF_PROJECT_ID, apiKey: process.env.VAIF_PUBLISHABLE_KEY,});
try { await vaif.from("posts").list();} catch (error) { if (error instanceof VaifRateLimitError) { console.warn(`rate limited, retry in ${error.retryAfter ?? 1}s`); } else if (error instanceof VaifError) { const code = error.code as VaifErrorCode; console.error(error.statusCode, code, error.message, error.requestId); } else { throw error; }}See the JavaScript SDK for the other error classes. For help, email support@vaif.studio with the requestId.