Errors
Every TotalBase API (the Data API, the Admin API, the App API) reports errors the same way. Codes are a stable contract: safe to branch on, never renamed.
The error body
{
"errors": [
{
"source": "POSTGRESQL",
"code": "CHECK_VIOLATION",
"message": "Not enough stock in this warehouse.",
"field": "quantity",
"operation": "part"
}
]
}
| Key | Present | Holds |
|---|---|---|
source | always | TOTALBASE for TotalBase's own checks, POSTGRESQL when the database refused |
code | always | the stable code: the tables below |
message | always | a readable explanation. For PostgreSQL errors it's PostgreSQL's own message, a RAISE EXCEPTION's text, or the constraint's comment when it has one. |
field | sometimes | the one column the error is about: a NOT NULL column, or the single column of a UNIQUE or CHECK. A form shows the error under it. |
operation | in transactions | the key of the transaction operation that failed |
The Data API never includes the SQL it generated. The SQL Console also shows PostgreSQL's detail, hint, SQLSTATE and position.
Handling errors by status
| Status | Meaning | Usually |
|---|---|---|
400 | the request is malformed or over a limit | fix the request; retrying won't help |
401 | no credential, or it's invalid or expired | sign in again or use a valid token |
403 | authenticated, but not allowed | the role lacks a permission or capability |
404 | doesn't exist, or isn't visible to you | check the name, the exposure, or the row filter |
409 | conflicts with the current state | another row, a newer version, a request in progress: reload or retry |
413 | too large | send less |
422 | a rule refused the values | show the message to the user and let them correct the values |
429 | too many requests | wait and retry |
500 | unexpected | check the server log; report it |
502, 504 | an external service failed or was too slow | retry later |
503 | PostgreSQL cancelled the statement (timeout) | narrow the query, or add an index |
A 404 deliberately looks the same whether something doesn't exist or you may not see it. A hidden table, a method that isn't exposed, a row outside your row filter and a DataView you can't open all answer as missing, so nobody can probe what exists.
Query and request
| Code | Status | Means |
|---|---|---|
INVALID_QUERY | 400 | the request is malformed: a bad parameter, a missing where on a collection update or delete, a filter value that isn't a string, a body that isn't an object |
INVALID_FIELD | 400 | a field that doesn't exist. Only administrators get this; a role with restricted fields gets FIELD_PERMISSION_DENIED. |
INVALID_OPERATOR | 400 | an operator that doesn't exist, such as _equals |
INVALID_TYPE | 400 | a value that doesn't fit the column: text for a number, a point not given as { lng, lat } |
INVALID_ALIAS | 400 | an alias that's malformed or used twice, or an aggregate without one |
INVALID_RELATION | 400 | a path through a column without a foreign key, or into a table that isn't exposed for reading |
INVALID_GROUP_BY | 400 | a malformed group_by |
INVALID_INTERVAL | 400 | a malformed $INTERVAL.*: the forms are 15m, 24h, 7d, 2w, 1y |
INVALID_PERIOD | 400 | a malformed $PERIOD.*: the form is $PERIOD.<period>.from or .to |
QUERY_FEATURE_DISABLED | 400 | _regex, a containment operator, having or count is turned off on this instance |
QUERY_STRING_TOO_LARGE | 400 | the query string is over api.max_query_string_bytes |
QUERY_PARAMETER_LIMIT_EXCEEDED | 400 | more top-level parameters than api.max_query_parameters |
QUERY_COMPLEXITY_LIMIT_EXCEEDED | 400 | more conditions, fields and relation steps than api.max_query_nodes, or more than 32 flags |
QUERY_DEPTH_LIMIT_EXCEEDED | 400 | a path deeper than api.max_relation_depth, or filters nested deeper than api.max_filter_depth |
REQUEST_BODY_TOO_LARGE | 413 | the body is over api.max_body_bytes |
RATE_LIMIT_EXCEEDED | 429 | too many requests from this address, or from this token on the MCP server |
Authentication
| Code | Status | Means |
|---|---|---|
AUTHENTICATION_REQUIRED | 401 | no credential, an unknown one, or a failed sign-in ("Invalid email or password", whatever the real reason) |
INVALID_TOKEN | 401 | a link token (invitation, reset, verification) that's invalid, expired or already used |
SESSION_EXPIRED | 401 | the session expired or was revoked: sign in again |
EMAIL_NOT_VERIFIED | 403 | the password was right but the email isn't verified yet. Offer to resend the link. |
INVALID_CURRENT_PASSWORD | 400 | changing a password with a wrong current password |
PASSWORD_TOO_SHORT | 400 | shorter than auth.password_min_length |
USER_TIMEZONE_LOCKED | 403 | the instance sets the time zone for everyone |
INVALID_TOKEN_EXPIRY | 400 | a token lifetime outside 1-3650 days, or a regeneration grace period outside 0-168 hours |
USER_API_NAME_DUPLICATE | 409 | the user already has a token with that name |
Permissions
| Code | Status | Means |
|---|---|---|
ACTION_DENIED | 403 | no ALLOW for the action - the message names it and the table: No READ grant on public.orders for your roles - or the row after a create or update falls outside the row filter. Nothing was written. |
VIEW_AS_READ_ONLY | 403 | a write while an administrator sees the App as someone else. Nothing was written. |
INVALID_VIEW_AS | 400 | X-TotalBase-View-As from a caller who isn't an administrator's session, or naming no active role or user. |
FIELD_PERMISSION_DENIED | 403 | a field you may not read, filter, sort or write. Fields are never dropped silently. |
FUNCTION_EXECUTION_DENIED | 403 | no EXECUTE on the function |
PERMISSION_DENIED | 403 | a rule that holds whatever the grants: an administrator-only table or column, a reserved role, an administrator's account, registration turned off |
PAYLOAD_CHECK_FAILED | 422 | a payload check refused the values. The message joins the labels of every failing check. |
TRANSACTION_EXECUTION_DENIED | 403 | the role lacks the Can run transactions capability |
EMAIL_SEND_DENIED | 403 | a workflow's executor lacks the Can send email capability |
Not found and conflicts
| Code | Status | Means |
|---|---|---|
RESOURCE_NOT_FOUND | 404 | the table, view or function doesn't exist, isn't exposed for this method, or is hidden. Also a DataView or object you can't open. |
ROW_NOT_FOUND | 404 | no row with that key, or it's outside your row filter |
ROW_VERSION_CONFLICT | 409 | If-Match with an old version: someone changed the row since you read it |
Writes
| Code | Status | Means |
|---|---|---|
INVALID_BULK_PAYLOAD | 400 | bulk=true without an array, an item without its primary key, or more items than api.bulk_max_items |
INVALID_ON_CONFLICT | 400 | on_conflict.target isn't exactly a primary key or UNIQUE constraint, or an UPDATE without on_conflict.data |
INVALID_NESTED_RELATION | 400 | a $child table that isn't exposed for POST, or has no foreign key to the parent |
AMBIGUOUS_NESTED_RELATION | 400 | the child table has more than one foreign key to the parent |
NESTED_FOREIGN_KEY_NOT_ALLOWED | 400 | a child sent its parent's foreign key; TotalBase sets it |
NESTED_MAX_DEPTH_EXCEEDED | 400 | deeper than api.nested_write_max_depth |
NESTED_MAX_RECORDS_EXCEEDED | 400 | more rows in the tree than api.nested_write_max_records |
INVALID_NESTED_PAYLOAD | 400 | a $child that isn't a list of objects |
UNSUPPORTED_NESTED_OPERATION | 400 | a $child on an update, with bulk=true or with on_conflict |
Transactions, idempotency and asynchronous calls
| Code | Status | Means |
|---|---|---|
TRANSACTIONS_DISABLED | 403 | transactions.enabled is off |
TRANSACTION_LIMIT_EXCEEDED | 400 | more operations than transactions.max_operations |
INVALID_TRANSACTION_PAYLOAD | 400 | a malformed operation: a missing key, table or id |
TRANSACTION_REFERENCE_NOT_FOUND | 400 | a $key.field reference to an unknown or later operation, or to a field its result doesn't have |
INVALID_IDEMPOTENCY_KEY | 400 | the key isn't 1-255 characters of A-Z a-z 0-9 _ . - |
IDEMPOTENCY_KEY_REQUIRED | 400 | the resource requires an Idempotency-Key |
IDEMPOTENCY_KEY_REUSED | 409 | the same key with a different request body |
IDEMPOTENCY_REQUEST_IN_PROGRESS | 409 | the first request with this key is still running: retry shortly |
IDEMPOTENCY_RESPONSE_TOO_LARGE_TO_REPLAY | 409 | the operation already ran once, but its response was too large to keep. It wasn't run again. |
ASYNC_EXECUTIONS_DISABLED | 409 | the background worker is off (async_executions.enabled) |
EXECUTION_NOT_FOUND | 404 | no such execution, or it's someone else's |
EXECUTION_NOT_CANCELLABLE | 409 | the execution already finished, or can't be stopped |
Functions
| Code | Status | Means |
|---|---|---|
FUNCTION_SIGNATURE_NOT_FOUND | 404 | no overload matches the argument names given |
AMBIGUOUS_FUNCTION | 422 | more than one overload matches |
PostgreSQL errors
When PostgreSQL refuses, the code is mapped from its SQLSTATE and source is POSTGRESQL.
| SQLSTATE | Code | Status | Typical cause |
|---|---|---|---|
23502 | NOT_NULL_VIOLATION | 422 | a required column left empty |
23503, 23001 | FOREIGN_KEY_VIOLATION | 409 | a reference to a row that doesn't exist, or deleting a row others still reference |
23505 | UNIQUE_VIOLATION | 409 | a duplicate value |
23514 | CHECK_VIOLATION | 422 | a CHECK constraint refused the row |
P0001 | RAISE_EXCEPTION | 422 | a trigger or function raised an error; its message is passed through |
22P02 | INVALID_TEXT_REPRESENTATION | 400 | a value PostgreSQL can't read as the column's type |
22001 | STRING_DATA_RIGHT_TRUNCATION | 422 | text longer than a varchar(n) allows |
22003 | NUMERIC_VALUE_OUT_OF_RANGE | 422 | a number too large for its column |
22012 | DIVISION_BY_ZERO | 422 | a division by zero in a function, view or generated column |
22007, 22008 | INVALID_DATETIME_FORMAT, DATETIME_FIELD_OVERFLOW | 400 | a malformed or impossible date |
428C9 | GENERATED_ALWAYS | 400 | a value sent for a column PostgreSQL computes |
40001 | SERIALIZATION_FAILURE | 409 | a concurrent transaction conflicted: retry |
40P01 | DEADLOCK_DETECTED | 409 | two transactions waited on each other: retry |
57014 | QUERY_CANCELED | 503 | the statement ran past its timeout, or was cancelled |
42501 | INSUFFICIENT_PRIVILEGE | 403 | TotalBase's database role lacks the privilege, or a trigger rule refused |
25006 | READ_ONLY_SQL_TRANSACTION | 400 | a write inside a read-only transaction, such as a dashboard query |
0A000 | FEATURE_NOT_SUPPORTED | 400 | something PostgreSQL doesn't do, such as truncating a table others reference |
42601 | SYNTAX_ERROR | 400 | invalid SQL (SQL Console, dashboard queries) |
42P01, 42703, 42704 | UNDEFINED_TABLE, UNDEFINED_COLUMN, UNDEFINED_OBJECT | 404 | the object doesn't exist |
42P07, 42701, 42710 | DUPLICATE_TABLE, DUPLICATE_COLUMN, DUPLICATE_OBJECT | 409 | the object already exists |
2BP01 | DEPENDENT_OBJECTS_STILL_EXIST | 409 | dropping something others depend on: drop those first, or use cascade |
42803 | GROUPING_ERROR | 400 | a column neither grouped nor aggregated |
42804 | DATATYPE_MISMATCH | 400 | incompatible types in an expression |
42883 | UNDEFINED_FUNCTION | 400 | no function or operator for those types |
42702 | AMBIGUOUS_COLUMN | 400 | a column name that matches more than one table |
other 22xxx | DATA_EXCEPTION | 400 | another invalid value |
other 42xxx | INVALID_STATEMENT | 400 | another problem with the statement |
Any other SQLSTATE is 500 INTERNAL_ERROR with source: POSTGRESQL.
Files and images
| Code | Status | Means |
|---|---|---|
FILE_ACCESS_DENIED | 403 | no permission on the file, or Files is turned off |
FILE_UPLOAD_REQUIRED | 400 | the upload has no file, or no file name |
FILE_TOO_LARGE | 413 | over files.max_file_size_bytes, or an email's attachments together over the limit |
FILE_MIME_TYPE_NOT_ALLOWED | 400 | the file's type isn't in files.allowed_mime_types |
FILE_STORAGE_ERROR | 502 | the file couldn't be written to or read from storage |
INVALID_TRANSFORM_PARAMS | 400 | a malformed image parameter, such as width=abc |
TRANSFORM_NOT_APPLICABLE | 400 | image parameters on a file that isn't an image |
TRANSFORM_DIMENSIONS_TOO_LARGE | 400 | the result would be larger than files.transforms.max_* |
TRANSFORM_DISABLED | 400 | image transformations are turned off |
TRANSFORM_PRESET_NOT_FOUND | 400 | an unknown preset |
TRANSFORM_TIMEOUT | 504 | the transformation took too long, or too many are running |
TRANSFORM_FAILED | 502 | the image couldn't be processed |
Email and templates
| Code | Status | Means |
|---|---|---|
SMTP_NOT_CONFIGURED | 409 | no SMTP relay set up: Settings › Email (SMTP) |
SMTP_CONNECTION_FAILED | 502 | the SMTP test couldn't connect or authenticate |
EMAIL_SEND_FAILED | 502 | the SMTP server refused the message |
TEMPLATE_NOT_FOUND | 404 | no such template |
TEMPLATE_ACCESS_DENIED | 403 | no READ on the template |
TEMPLATE_WRONG_KIND | 400 | a PDF template used for an email or the other way round, or an email template without a subject or body |
MISSING_TEMPLATE_VARIABLES | 400 | the template needs values that weren't given |
TEMPLATE_PDF_GENERATION_FAILED | 500 | the PDF couldn't be produced |
TEMPLATE_NAME_DUPLICATE, TEMPLATE_SLUG_DUPLICATE | 409 | another template has that name or slug |
Workflows
| Code | Status | Means |
|---|---|---|
WORKFLOW_NOT_FOUND | 404 | no such workflow, or it has no published version |
WORKFLOW_DISABLED | 409 | the workflow is turned off |
WORKFLOW_NOT_RUNNABLE | 409 | it starts from its own trigger (a data event, a schedule), not on demand |
WORKFLOW_NOT_ACTIVATABLE | 422 | turning on a workflow that has never been published |
WORKFLOW_NOT_PUBLISHABLE | 422 | the draft isn't ready to run: a step is missing something it needs |
WORKFLOW_INVALID_DESIGN | 400 | the graph or a step's configuration is invalid |
WORKFLOW_REVISION_CONFLICT | 409 | the workflow was changed by someone else since it was read |
WORKFLOW_NAME_CONFLICT, WORKFLOW_SLUG_CONFLICT | 409 | another workflow has that name or slug |
WORKFLOW_VALIDATION_FAILED | 422 | the input doesn't match the workflow's input form |
WORKFLOW_WEBHOOK_INVALID_PAYLOAD | 400 | a webhook body that isn't a JSON object or array |
WORKFLOW_SYNC_TIMEOUT | 504 | a synchronous call waited past its limit. The run continues; meta.run identifies it. |
WORKFLOW_LIMIT_EXCEEDED | 422 | a run went over a limit: operations per run, an HTTP response's size |
WORKFLOW_EXECUTION_FAILED | 422 | a step failed |
WORKFLOW_HTTP_DESTINATION_BLOCKED | 403 | an HTTP or SOAP request step aimed at an address the instance doesn't allow |
WORKFLOW_SOAP_FAULT | 502 | a SOAP request step's service answered a SOAP Fault; the message carries its text |
WORKFLOW_SIGN_FAILED | 422 | a Sign step's certificate or private key can't be used: not PEM, a wrong passphrase, not RSA, not a pair, or expired |
WORKFLOW_RUN_NOT_FOUND | 404 | no such run |
WORKFLOW_RUN_NOT_CANCELLABLE | 409 | the run already finished, or isn't running in this process |
DataViews
| Code | Status | Means |
|---|---|---|
DATAVIEW_NOT_FOUND | 404 | no such DataView, or no published version you may open |
DATAVIEW_VERSION_NOT_FOUND | 404 | no such version |
DATAVIEW_NAME_DUPLICATE, DATAVIEW_SLUG_DUPLICATE | 409 | another DataView has that name or slug |
DATAVIEW_SOURCE_TYPE_INVALID | 422 | the source isn't a table or view, or a page that writes records has a view as its source |
DATAVIEW_DETAIL_REQUIRED, DATAVIEW_DETAIL_INVALID | 422 | a master-detail without a detail table, or one with no foreign key to the master |
DATAVIEW_VIEW_TYPE_NOT_SUPPORTED | 422 | an unknown layout |
DATAVIEW_VIEW_CONFIG_INVALID | 422 | the layout's settings don't fit the table, such as a tree without a parent column |
DATAVIEW_FRAME_URL_INVALID | 422 | an external frame's address isn't valid |
DATAVIEW_COLUMN_INVALID | 422 | setting on a DataView's column what belongs to the table's column (label, format) |
DATAVIEW_FIELD_NOT_FOUND, DATAVIEW_FIELD_DUPLICATE, DATAVIEW_FIELD_NOT_DELETABLE | 404, 409, 422 | a column or widget that doesn't exist, is already there, or is the primary key |
DATAVIEW_FIELD_WIDGET_INVALID, DATAVIEW_WIDGET_INVALID, DATAVIEW_WIDGET_SLOT_INVALID, DATAVIEW_WIDGET_NOT_ALLOWED | 422 | an unknown widget, a wrong configuration, a wrong place for it, or a page that takes no widgets |
DATAVIEW_VARIABLES_INVALID | 422 | a page's variables are invalid |
DATAVIEW_STATE_NOT_FOUND, DATAVIEW_STATE_NAME_DUPLICATE, DATAVIEW_STATE_SHARE_SELF | 404, 409, 422 | saved views: not found, a name already used, or sharing one with yourself |
INVALID_DATAVIEW_COLOR | 422 | a color outside the palette |
INVALID_COLUMN_PRESENTATION, INVALID_ROW_PRESENTATION | 422 | a column's or table's presentation is invalid |
BULK_EDIT_TOO_MANY | 422 | a bulk edit over the row limit: edit in parts |
Dashboards and embeds
| Code | Status | Means |
|---|---|---|
DASHBOARD_NOT_FOUND, DASHBOARD_VERSION_NOT_FOUND | 404 | no such dashboard or version |
DASHBOARD_NAME_DUPLICATE, DASHBOARD_SLUG_DUPLICATE | 409 | another dashboard has that name or slug |
DASHBOARD_WIDGET_NOT_FOUND, DASHBOARD_VARIABLE_NOT_FOUND | 404 | no such widget or variable |
DASHBOARD_WIDGET_NOT_CONFIGURED | 422 | the widget has no query yet |
DASHBOARD_QUERY_INVALID | 422 | the query uses something widgets can't, such as $USER.* without a signed-in user |
DASHBOARD_WIDGET_QUERY_FAILED | 422 | the widget's query failed in PostgreSQL |
DASHBOARD_WIDGET_QUERY_TIMEOUT | 422 | the query ran past dashboards.query_timeout_ms |
DASHBOARD_PUBLISH_BROKEN_WIDGETS | 422 | not published: some widgets' queries fail |
DASHBOARD_WIDGET_INVALID, DASHBOARD_WIDGET_REFERENCE_INVALID, DASHBOARD_WIDGET_ROLES_INVALID | 422 | a widget's type, size, configuration, references or roles are invalid |
DASHBOARD_COMBO_NEEDS_TWO_VALUES, DASHBOARD_PIVOT_AGGREGATE_REQUIRED, DASHBOARD_PIVOT_TOO_MANY_COLUMNS, DASHBOARD_WIDGET_NEGATIVE_VALUE | 422 | the query's result doesn't fit the widget: a combo chart needs two values, a pivot needs aggregates and at most a set number of columns, some charts take no negative values |
DASHBOARD_VARIABLE_INVALID, DASHBOARD_VARIABLE_RESERVED | 422 | a variable's definition is invalid, or uses a reserved name |
DASHBOARD_TABS_INVALID, DASHBOARD_TAB_NOT_EMPTY | 422, 409 | an unknown tab, or removing a tab that still holds widgets |
INVALID_DASHBOARD_COLOR, INVALID_DASHBOARD_REFRESH_INTERVAL | 422 | a color outside the palette, or an unsupported auto-refresh interval |
DASHBOARD_SPEC_INVALID, DASHBOARD_DRAFT_CHANGED | 422, 409 | a dashboard description (as agents send it) is invalid, or the draft changed since it was read |
DASHBOARD_FILE_TOKEN_INVALID, DASHBOARD_FILE_NOT_ALLOWED | 403 | a file link that expired, or a private file in an embed that doesn't allow files |
EMBED_TOKEN_INVALID | 401 | an embed link or signed token that's missing, invalid, expired, or doesn't give a variable the embed locks |
EMBED_NEEDS_UPDATE | 409 | the embed locks a variable the dashboard no longer has |
DASHBOARD_EMBED_INVALID, DASHBOARD_EMBED_NOT_FOUND, DASHBOARD_EMBED_NAME_DUPLICATE | 422, 404, 409 | an embed's settings are invalid, it doesn't exist, or its name is taken |
Auth providers
| Code | Status | Means |
|---|---|---|
AUTH_PROVIDER_NOT_FOUND | 404 | no enabled provider with that name |
INVALID_AUTH_PROVIDER_PAYLOAD | 400 | a provider's configuration is incomplete, such as an OIDC provider without an issuer |
AUTH_PROVIDER_NAME_DUPLICATE | 409 | another provider has that name |
OAUTH_REDIRECT_NOT_ALLOWED | 400 | redirect_to isn't in the provider's allowed redirect URLs |
INVALID_OAUTH_STATE | 401 | the sign-in attempt was tampered with, expired, or started for another provider |
OAUTH_EMAIL_REQUIRED | 400 | the provider didn't share an email address |
OAUTH_UPSTREAM_ERROR | 502 | the provider couldn't be reached, or answered unexpectedly |
Administration
| Code | Status | Means |
|---|---|---|
INVALID_SETTING | 400 | a setting value outside its type or range |
INVALID_SCHEMA_OPERATION | 400 | an invalid identifier, type or expression in a schema change, or an unsupported option |
SQL_CONSOLE_DISABLED | 403 | the SQL Console is turned off, or turned off for tokens |
MAINTENANCE_ALREADY_QUEUED, MAINTENANCE_BATCH_RUNNING | 409 | that maintenance is already queued, or a batch is running |
INVALID_SECRET_KEY | 400 | a secret's key isn't lowercase letters and digits with ., _ or - |
SECRET_NOT_FOUND | 404 | no such secret |
SECRET_DECRYPTION_FAILED | 500 | a stored secret can't be decrypted, usually because the master key changed |
INVALID_NAME, INVALID_DESCRIPTION, INVALID_SLUG | 400 | a name, description or slug that's empty, too long, or has characters it can't have |
INVALID_MENU_PAYLOAD | 400 | the App menu's arrangement is malformed or nested too deep |
Startup
These stop the server before it accepts requests. They appear in the server log, never in a response.
| Code | Means |
|---|---|
INVALID_MASTER_KEY | the master key isn't the one this installation was started with - or there is none (no variable, no master.key) for an installation that has one |
MIGRATION_CHECKSUM_MISMATCH | an already applied migration file was changed |
TOTALBASE_SCHEMA_CONFLICT | a schema named totalbase exists and holds objects TotalBase didn't create |
Unexpected
| Code | Status | Means |
|---|---|---|
INTERNAL_ERROR | 500 | something unexpected, with a generic message. The details are in the server log. |