Error codes
Every stable error code the API can return, and what each one means.
When a request fails, the response body carries a stable machine-readable code alongside the human-readable message:
{
"error": {
"error": "App database is not provisioned yet",
"code": "APP_NOT_PROVISIONED",
"details": { "appUid": "..." }
}
}Branch on error.code, never on error.error. The code is the contract and will not
change; the message is prose and may be reworded at any time. details carries whatever
context is useful for that specific failure.
The Status column is the HTTP status the code is actually thrown with, verified against the real throw sites by a test in the backend suite rather than maintained by hand.
Not every failure carries a code. A generic policy denial returns 403 with code: null,
so treat error.code as present-or-absent rather than assuming one is always there.
There are 378 codes.
App lifecycle
| Code | Status | Meaning |
|---|---|---|
APP_NOT_PROVISIONED | 409 | The target app has no provisioned database yet. Provision the app before calling its API. |
APP_NOT_FOUND | 404 | No such app within your organization. Returned identically for an app that does not exist and one owned by another organization. |
ROUTE_NOT_FOUND | 404 | No route answers this method and path. Check the URL, the method, and the app uid. |
AutoCRUD write failures
| Code | Status | Meaning |
|---|---|---|
CREATE_REQUIRED_FIELD_MISSING | 400 | A column the table requires was absent from the request body. |
CREATE_DUPLICATE | 409 | The row violates a unique constraint on the table. |
The row edge (helpers/rowEdge.js)
| Code | Status | Meaning |
|---|---|---|
ROW_FIELD_NOT_ACCEPTED | 400 | A field the data API does not take: a row's internal id, its uid (minted by the platform), a scope or actor column, or a raw foreign-key column. The message names the field to use instead. |
ROW_NOT_FOUND | 404 | No row with that uid that you can reach: it does not exist, was deleted, or belongs to another organization (the same answer for all three). In a batch, details.index names it. |
ROW_REFERENCE_NOT_FOUND | 400 | A <column>_uid reference names no row in the table it points at. The message names the column. |
ROW_EDGE_COLLISION | 409 | The table cannot be served: a foreign-key column's uid name (<column>_uid) is also the name of a real column. Rename one of them. |
ROW_ORG_REQUIRED | 400 | The table's rows belong to an organization: name the app organization a new row belongs to (orgUid in the console and agent tools, the row's org_uid in a handler acting for the app's owner). An app with no organizations yet cannot take rows in such a table. |
ROW_ORG_NOT_FOUND | 400 | orgUid (or a row's org_uid) names no organization in the target app that the caller reaches. |
PAGE_CURSOR_INVALID | 400 | A page read with after was given a cursor this API did not issue for that table (changed, from another table, or sealed under a retired secret), or a sort or offset, which keyset paging does not take. |
QUERY_INVALID | 400 | A list query this API cannot run: malformed JSON in where, an unknown operator, a column the table does not serve, or a sort direction other than asc or desc. |
QUERY_TOO_COMPLEX | 400 | A list query past the bounds one request may ask for: too many conditions, nested too deep, too many $in values, too many sort columns, or a where that is too long. details.limits names them. |
BATCH_TOO_LARGE | 400 | A batch write carries more rows than one request may (details.maxRows). Split it. |
QUERY_TIMEOUT | 422 | A list query ran longer than one request may. Narrow it, or add an index on the columns it filters or sorts by. |
PAGE_CURSOR_UNAVAILABLE | 500 | The server has no secret to seal page cursors with (ACCESS_TOKEN_SECRET is not set). |
AGENT_ORG_UNRESOLVED | 403 | The agent belongs to no organization of the app it is acting on, and is not the app owner's. |
APP_ORG_UNBOUND | 500 | Platform code tried to reach an app's data without saying which app organization it acts in. |
ORG_REACH_OWNER_ONLY | 403 | Only the app's owner, in the console, can change what its agents, jobs and keys may reach. |
ORG_REACH_NOT_APPLICABLE | 409 | This agent, job or key belongs to one of the app's organizations and acts only there; org reach is for the owner's own. |
STORAGE_SHARED_READ_ONLY | 403 | This was shared with you by the app's owner: you can open and download it, and add your own files beside it, but not change it or add folders inside it. |
STORAGE_SHARING_OWNER_ONLY | 403 | Only the app's owner decides what its folders and files are shared with. |
STORAGE_SHARING_NOT_APPLICABLE | 409 | This belongs to one of the app's organizations; sharing is for the owner's own folders and files. |
STORAGE_OUT_OF_REACH | 403 | That reaches organizations outside the caller's org reach. |
Handler resolution (helpers/handlerResolver.js)
| Code | Status | Meaning |
|---|---|---|
HANDLER_NOT_FOUND | 404 | No live handler in this app has that name. |
HANDLER_NAME_AMBIGUOUS | 409 | More than one live handler in this app has that name, so none of them runs. Rename or archive one. |
HANDLER_KIND_BOUND | 409 | An endpoint still runs this handler: unassign it from its endpoint actions before marking it job-only. |
HANDLER_TRIGGER_REQUIRED | 500 | Platform code dispatched a handler without saying why it runs it. A defect, never a caller's mistake. |
HANDLER_PLATFORM_ONLY | 403 | This handler runs only for the platform: its scheduled jobs, or the app owner testing it in the console. |
Table protection
| Code | Status | Meaning |
|---|---|---|
TABLE_LOCKED | 423 | The table is locked against this operation by its protection settings. |
COLUMN_REDACTED | 400 | That column is redacted, so it cannot be filtered or sorted on. |
Review, then confirm
| Code | Status | Meaning |
|---|---|---|
REVIEW_HASH_MISMATCH | 409 | What you confirmed is not what was reviewed. Review again, then confirm. |
PLAN_HASH_MISMATCH | 409 | The table changed since this plan was made. Plan again, then apply the new plan. |
POLICY_IS_SYSTEM | 423 | That policy is seeded platform wiring and cannot be unbound or edited. Bind or unbind your own policy instead. |
Custom handlers / execution
| Code | Status | Meaning |
|---|---|---|
HANDLER_NOT_IMPLEMENTED | 501 | The action is registered on the endpoint but has no handler implementation published. |
HANDLER_WORKER_UNAVAILABLE | 503 | The isolated handler runtime could not be reached. Transient — retry. |
HANDLER_WORKER_BUSY | 503 | The isolated handler runtime is at capacity (overall, or for this app). Transient — retry. |
Definition as code (notes/app-definition-as-code-plan.md)
| Code | Status | Meaning |
|---|---|---|
DEFINITION_CODE_MANAGED | 409 | The object is managed by the app's connected repository. Change it there and let the deployment apply it, or have the owner turn on break-glass editing for an incident. |
DEFINITION_BUNDLE_INVALID | 422 | The definition bundle could not be read: a missing or invalid authoric.json, an unknown folder or key, or a file that fails a check. The message names the file and the fix. |
DEFINITION_PLAN_INVALID | 422 | The bundle is valid but cannot be applied to this app as it is: something it removes or changes is still used. The message says what to change first. |
DEFINITION_SOURCE_UNAVAILABLE | 409 | The deployment's source could not be read: the app has no connected repository, the connection was removed, or an uploaded bundle is no longer held. |
DEFINITION_OWNER_ONLY | 403 | Connecting a repository, the deployment safety settings, break-glass and deploy keys are changed only from the app owner's own console session, never with a key or token. |
APP_TYPES_OWNER_ONLY | 403 | An app's types are read from the owner's own console session, or with a deploy key for the app. |
DEFINITION_REPO_LINKED | 409 | The app is already connected to a repository. An app deploys from one; disconnect it first, or change its branch or bundle path. |
DEPLOYMENT_SELF_APPROVAL | 403 | You caused this deployment, so someone else has to approve it. An owner can turn off Prevent self-approval for a one-person team. |
DEPLOYMENT_NOT_WAITING | 409 | The deployment is not in a state that allows this: it is no longer waiting for approval, or has already started applying. |
DEPLOYMENT_PLAN_CHANGED | 409 | What the deployment would apply changed after it was planned, so it was stopped. Deploy again to plan it afresh. |
DEPLOYMENT_NOT_ROLLBACKABLE | 409 | That deployment cannot be rolled back to: it never went live, or it did not record the versions it ran. |
Billing / plan changes
| Code | Status | Meaning |
|---|---|---|
PLAN_CHANGE_BLOCKED | 409 | The target plan does not fit the organization's current usage. Raised in EITHER direction: a plan's rank decides how it is billed, not which way each individual limit moved, so an upgrade that tightens one limit is refused too. details.blockers lists every resource that is over, with the usage and the target limit, so the customer can free them all at once instead of being refused repeatedly. |
PLAN_CHANGE_ALREADY_SCHEDULED | 409 | A plan change is already scheduled for the end of the current period. Cancel it before scheduling another — a second would orphan the first schedule in Stripe. |
NO_PENDING_PLAN_CHANGE | 409 | There is no scheduled plan change to cancel. |
PLAN_PAYMENT_FAILED | 402 | The card could not be charged for an upgrade, so the plan was NOT changed — the whole change is rolled back rather than granted unpaid. Update the payment method and retry. |
Billing / plan limits
| Code | Status | Meaning |
|---|---|---|
SUBSCRIPTION_LIMIT_REACHED | 402 | The organization's plan limit for this resource is reached. Upgrade the plan or free capacity. |
AI_SPEND_CAP_REACHED | 402 | An AI spend limit set by this account has been reached. Raise the limit in settings to continue. |
FEATURE_NOT_INCLUDED | 402 | The organization's plan does not include this feature. Upgrade the plan to enable it. |
Account lifecycle locks
| Code | Status | Meaning |
|---|---|---|
ACCOUNT_RESTRICTED | 423 | The account is read-only because a payment failed. Reads still work; bring the subscription current to write again. details: { state, reason: 'lifecycle', stateReason } |
ACCOUNT_PAUSED | 423 | The app is paused — for inactivity (restore it from the console) or for non-payment (bring the subscription current). details.stateReason says which. details: { state, reason: 'lifecycle', stateReason } |
APP_WAKING | 503 | The app was asleep after a period of no traffic and this request started waking it. Retry after the Retry-After interval. details: { retryAfterSeconds, state, stateReason } |
APP_RESTORING | 503 | The app is being restored from its archive and cannot serve yet. Retry after the Retry-After interval. details: { appUid, retryAfterSeconds } |
ACCOUNT_TORN_DOWN | 423 | The app's live data was removed after a long pause. It can still be restored from its archive until the archive expires. details: { state, reason: 'lifecycle', stateReason } |
Data placement
| Code | Status | Meaning |
|---|---|---|
APP_MIGRATING | 423 | The app is being moved to different infrastructure and is briefly read-only. Reads continue; writes resume automatically when the move completes. |
PLACEMENT_NOT_ENTITLED | 403 | The organization's plan does not include the requested data placement tier. |
PLACEMENT_PREFLIGHT_FAILED | 402 | The app does not fit the requested placement tier — free capacity or choose a larger tier. |
PLACEMENT_MIGRATION_IN_PROGRESS | 409 | Another placement move is already running for this app. Only one may run at a time. |
PLACEMENT_DESTINATION_UNAVAILABLE | 409 | The requested placement tier is not available on this deployment yet (for example, S3 is not configured). A queued move resumes once it is. |
Platform configuration
| Code | Status | Meaning |
|---|---|---|
EMAIL_NOT_CONFIGURED | 500 | This deployment has no email sender configured. An operator must configure one. |
Email connections and app email (notes/email-connections-and-branding-plan.md)
| Code | Status | Meaning |
|---|---|---|
EMAIL_CONNECTION_INVALID | 400 | An email connection's server settings are not usable: a mail host, an SMTP submission port, and starttls or tls. |
EMAIL_SENDER_INVALID | 400 | A sender is not an address (name@example.com) or a domain (@example.com), or the default sender is not one of the connection's senders. |
EMAIL_HOST_BLOCKED | 400 | The mail host resolves to a private or reserved address, which the platform never dials. |
EMAIL_CONNECTION_FAILED | 502 | The mail server could not be reached, refused the login, or failed the secure connection. The message says which. |
EMAIL_CONNECTION_NOT_FOUND | 404 | No email connection or grant by that uid in this org or app. |
EMAIL_CONNECTION_UNVERIFIED | 409 | The email connection has not passed a test since it was connected or last edited, so it cannot be granted or used. |
EMAIL_ALIAS_IN_USE | 409 | That alias already names another email connection in this app. Revoke it first to point it elsewhere. |
EMAIL_NOT_GRANTED | 403 | The app has no email connection by that name (or no default): its owner grants one under App settings. |
EMAIL_FROM_NOT_ALLOWED | 403 | The from address is not one of the email connection's senders. |
EMAIL_PLACEHOLDER_MISSING | 400 | A {key} or {{key}} placeholder in the message names no value in its data, so nothing was sent. |
EMAIL_MESSAGE_INVALID | 400 | The message is missing what every email needs (a recipient, a subject, some content) or has a malformed address. |
EMAIL_SEND_FAILED | 502 | The app's mail server did not accept the message. It is logged as failed, with the server's reason. |
App branding (notes/email-connections-and-branding-plan.md §5)
| Code | Status | Meaning |
|---|---|---|
BRANDING_INVALID | 400 | A branding field is not usable: a color that is not #rrggbb, a URL that is not https, an address that is not an email address, or text past its length. The message names the field. |
BRANDING_ASSET_NOT_FOUND | 404 | No file by that uid among the app owner's own files, so it cannot be the logo or icon. |
BRANDING_ASSET_NOT_PUBLIC | 400 | The file is in a private storage, so an email cannot show it. Move it to a public storage, or make its storage public. |
SMS_NOT_CONFIGURED | 500 | This deployment has no SMS sender configured. An operator must configure one. |
SMS_CHANNEL_UNSUPPORTED | 501 | The configured SMS provider cannot send this kind of message. Switch to the Programmable Messaging adapter. |
SMS_SUPPRESSED | 409 | That number has opted out of messages from this sender and cannot be texted. |
ARBITER_NO_LLM | 409 | No AI provider is connected for this organization. Connect one in Integrations. |
AI_NO_LLM | 409 | No AI provider is connected for this organization. Connect one in Integrations. |
AI_MODEL_UNAVAILABLE | 409 | The configured AI model is no longer available from this provider. Choose a replacement to resume. |
LLM_MODEL_NOT_APPROVED | 400 | That AI model is not approved for this provider. Choose one from the model list. |
AI_TIER_INVALID | 400 | sdk.ai takes a tier ("fast" or "default"), not a model. Set the models on your AI connection. |
Sign-up doors (api/notes/staff-members-and-authz-hardening-plan.md §2.1)
| Code | Status | Meaning |
|---|---|---|
SIGNUP_ORG_NOT_ALLOWED | 400 | Sign-up cannot join an existing organization; accept an invitation to join one. |
STAFF_SIGNUP_NOT_ALLOWED | 403 | Staff accounts are created by setup or by existing staff, never by a public sign-up. |
First-run setup (api/notes/staff-members-and-authz-hardening-plan.md §5)
| Code | Status | Meaning |
|---|---|---|
STAFF_SETUP_DONE | 409 | Setup has already been completed on this install. |
STAFF_SETUP_TOKEN_MISSING | 403 | This install has no setup token; run npm run staff:setup on the server. |
STAFF_SETUP_TOKEN_INVALID | 403 | The setup token is not valid. |
STAFF_SETUP_INVALID | 400 | Setup needs a valid email and a password of at least 8 characters. |
STAFF_EMAIL_TAKEN | 409 | A staff member with that email already exists. |
STAFF_SETUP_REQUIRED | 409 | Setup has not run on this install; there is no staff organization yet. |
Invite links (api/notes/staff-members-and-authz-hardening-plan.md §2.7)
| Code | Status | Meaning |
|---|---|---|
INVITE_TOKEN_INVALID | 400 | That invitation link is not valid, has expired, or was already used. |
INVITE_EMAIL_MISMATCH | 403 | This invitation was sent to a different email address. |
INVITE_ACCOUNT_TYPE_MISMATCH | 403 | This invitation is for a different kind of account. |
INVITE_REQUIRED | 403 | Staff accounts are created by invitation only. |
Platform access (notes/platform-access-plan.md)
| Code | Status | Meaning |
|---|---|---|
FEEDBACK_EMPTY | 400 | Write something before sending (up to 5,000 characters). |
FEEDBACK_NO_SENDER | 401 | Sign in to send feedback. |
FEEDBACK_NOT_FOUND | 404 | That feedback does not exist, or it was removed. |
FEEDBACK_STATE | 400 | Feedback is new, read or archived. |
FEEDBACK_ATTACHMENT_INVALID | 400 | Attach up to 3 images (PNG, JPEG or WebP), up to 5 MB each. |
NOTIFICATION_TYPE_UNKNOWN | 400 | There is no email notification of that type. |
NOTIFICATION_TYPE_LOCKED | 400 | This email is always sent: it warns before data is deleted. |
BOT_CHECK_FAILED | 403 | Please complete the bot check and try again. |
BOT_CHECK_UNAVAILABLE | 503 | The bot check could not be completed. Try again in a moment. |
BOT_CHECK_SETTINGS_INVALID | 400 | A bot check setting is not usable: a site key or secret key that is not a Turnstile key, or turning the check on without both keys. The message names the field. |
BOT_CHECK_SECRET_INVALID | 400 | Cloudflare does not recognize that Turnstile secret key. Copy it again from the widget's settings in Cloudflare. |
ALLOWED_ORIGIN_INVALID | 400 | An allowed domain is not usable: it must be an https origin such as https://yourapp.com or https://*.yourapp.com, with no path. The message names the entry. |
ALLOWED_ORIGINS_TOO_MANY | 400 | An app can allow up to 25 domains. Use a wildcard such as https://*.yourapp.com for many subdomains. |
CORS_ORIGIN_NOT_ALLOWED | 403 | This website is not allowed to call this API from a browser. Add its domain to the app's allowed domains. |
RATE_LIMITED | 429 | Too many requests. Wait the number of seconds in the Retry-After header, then try again. |
UPLOAD_NOT_CONFIRMABLE | 400 | This upload can only be confirmed by whoever requested it, with the confirmToken its presign returned, before it expires. |
IDEMPOTENCY_KEY_REUSED | 409 | This Idempotency-Key was already used for a different request. Use a new key for a new request. |
IDEMPOTENCY_IN_PROGRESS | 409 | A request with this Idempotency-Key is still in progress. Wait for it, then retry with the same key. |
REDIRECT_NOT_ALLOWED | 400 | redirectTo must be an http(s) address on one of the app's allowed domains, or on localhost. |
SIGNUP_INVITE_ONLY | 403 | Authoric is invite-only right now. Request access, or use the sign-up link from your invitation. |
PLATFORM_ACCESS_REVOKED | 403 | Access to Authoric has been revoked for this account. |
PLATFORM_ACCESS_LINK_INVALID | 400 | That sign-up link is not valid, has expired, or was already used. |
PLATFORM_ACCESS_NOT_FOUND | 404 | No such access record. |
PLATFORM_ACCESS_STATE | 409 | That change does not fit this person's current access. |
Scheduled jobs: arming (api/notes/staff-members-and-authz-hardening-plan.md §2.4)
| Code | Status | Meaning |
|---|---|---|
CRON_ROLE_OUTSIDE_ORG | 403 | A scheduled job can only be given a role your organization owns. |
CRON_BUSY | 409 | The job is running right now. Try again once its current turn has finished. |
JOB_ITEM_TIMEOUT | 504 | One item of a scheduled job took longer than the per-item limit (60 seconds), and was skipped. |
Roster changes from handler code (sdk.roles)
| Code | Status | Meaning |
|---|---|---|
PRINCIPAL_NOT_HUMAN | 403 | Only people are given roles from handler code. |
ROLE_ORG_MISMATCH | 403 | A role is granted inside the caller's own organization. |
ROLE_NOT_FOUND | 404 | That role does not exist. |
MEMBER_NOT_FOUND | 404 | That member is not in this organization. |
USER_NOT_FOUND | 404 | That user does not exist. |
App creation (api/notes/staff-members-and-authz-hardening-plan.md §2.3)
| Code | Status | Meaning |
|---|---|---|
CREATE_APP_ORG_MISMATCH | 403 | An app can only be created in your active organization. |
CREATE_APP_TENANT_SELECTOR | 400 | tenantId / tenantEmail are not accepted; the app is created for your organization's own account. |
Policy scope (api/notes/staff-members-and-authz-hardening-plan.md §2.2)
| Code | Status | Meaning |
|---|---|---|
POLICY_ROLE_SCOPE_MISMATCH | 403 | An organization's policy can only be bound to that organization's own roles; shared roles take only seeded policies. |
Staff (api/notes/staff-members-and-authz-hardening-plan.md)
| Code | Status | Meaning |
|---|---|---|
ORG_NOT_ADMITTED | 403 | This account cannot be a member of that organization. |
STAFF_CAPABILITY_NOT_GRANTABLE | 403 | Those actions belong to Authoric staff and can only be granted inside the staff organization. |
ORG_NOT_BILLED | 404 | This organization is not billed; it has no plan or usage to show. |
STAFF_ONLY | 403 | This action is for Authoric staff only. |
PLATFORM_TARGET_STAFF_ONLY | 403 | The platform database is not an app target; name one of your organization's apps as targetAppUid. |
ACT_AS_INVALID | 400 | That act-as request names something we cannot act on. |
ACT_AS_REASON_REQUIRED | 400 | Say why you need access to this organization; the reason is recorded and shown to the customer. |
ACT_AS_NOT_HUMAN | 403 | Only a person can act as an organization. |
ACT_AS_STAFF_ORG | 400 | The staff organization is not something to act as. |
ACT_AS_NOT_A_MEMBERSHIP | 409 | You are acting as this organization, not a member of it — switch back to stop. |
MEMBER_TOKEN_SELF_ONLY | 403 | An approver token can only be minted for your own account. |
MEMBER_TOKEN_STAFF_ORG | 403 | Approver tokens cannot be minted in the staff organization; staff act through sessions. |
AI_MODEL_INVALID | 400 | The AI model catalog change is invalid; the message says which field and why. |
AI_MODEL_NOT_FOUND | 404 | No such model in the AI model catalog. |
AI_MODEL_EXISTS | 409 | That provider and model id are already in the AI model catalog. |
AI_MODEL_IN_USE | 409 | The model is still in use. It fills a platform role (reassign the role first), or AI connections chose it and the change needs confirming. details lists what points at it. |
AI_MODEL_NOT_VERIFIED | 409 | Verify the model before approving it: a verification run proves it works through the platform's adapter. |
AI_MODEL_UNPRICED | 409 | The model has no price, so its spend would not count toward AI spend caps. Add a price, or confirm running it unpriced. |
LLM_CONNECTION_NOT_FOUND | 404 | No such AI connection in the active organization. |
AI_SOURCE_REDACTED | 400 | A derived column cannot read from a redacted column. Remove it from the source list or un-redact it. |
AI_DERIVED_COLUMN_INVALID | 400 | The derived-column configuration is invalid. |
AI_EMBEDDINGS_UNSUPPORTED | 409 | The connected AI provider does not offer embeddings. Semantic search needs a provider that does. |
APPROVAL_SELF_APPROVAL | 403 | The caller raised this approval request and cannot approve it themselves. |
HANDLER_RUN_NO_PRINCIPAL | 409 | Run live executes as a principal of the app, and a console session is not one. Use the dry run, or call the endpoint as one of the app's users. |
APPROVAL_CONSUMED | 409 | This approval was already used by another run of the same work. An approval runs its work once. |
APPROVAL_ORG_REQUIRED | 403 | Approvals belong to an organization, and this caller has no active organization. |
STEP_ALREADY_DECIDED | 409 | This approval step was already decided by another surface. Refresh to see the current state. |
API_KEY_INVALID | 401 | The API key is missing, malformed, unknown, revoked, or expired. Returned identically for all five. |
SDK-capability governance (notes/handler-sdk-capability-governance.md)
| Code | Status | Meaning |
|---|---|---|
SDK_CAP_NOT_GRANTED | 403 | You lack an SDK capability the handler's code uses. Request the capability to author it. details: { missing: [{ capability, label, risk, methods }], actor } — the capabilities required but not held. |
DDL_NOT_DRY_RUNNABLE | 422 | A dry-run tried to execute schema-changing SQL. DDL cannot be rolled back, so it is never dry-runnable. |
SDK_CAP_NOT_IN_MANIFEST | 403 | The handler called an SDK power absent from the manifest frozen at publish time. Republish to refresh it. |
SDK_UNKNOWN_METHOD | 400 | The handler called an SDK method that does not exist. Check the spelling against the editor's autocomplete — the contract d.ts is the full surface. |
LINK_TOKEN_INVALID | 404 | The link is invalid, expired, already used, revoked, or minted for a different purpose. Returned identically for all five. |
Agent/arbiter charts (services/charts/chartSpec.js + the chart handlers)
| Code | Status | Meaning |
|---|---|---|
CHART_SPEC_INVALID | 422 | The chart spec failed structural validation. details: { issues: string[] } — the specific validation failures. |
CHART_SNAPSHOT_TOO_LARGE | 413 | The chart is well-formed but its frozen data snapshot exceeds the storage/serve cap. details: { maxRows, maxBytes } — the caps that were exceeded. |
CHART_NOT_FOUND | 404 | No such chart within your scope. Returned identically for a chart that does not exist and one owned by another organization. |
CROSS_PLANE_FORBIDDEN | 403 | An app-plane caller tried to address another plane. An app may only touch its own database. |
GRANT_EXCEEDS_HOLDER | 403 | You tried to grant capabilities you do not hold yourself. The response names which ones; ask someone who holds them, or have them granted to you first. |
ROLE_PROTECTED | 400 | Built-in roles cannot be deleted. Create a custom role instead, or unbind this one from whoever holds it. |
Agent run lifecycle (notes/agent-run-lifecycle-and-reliability-plan.md)
| Code | Status | Meaning |
|---|---|---|
AGENT_RUN_NOT_FOUND | 404 | No such agent run within your scope. Returned identically for a run that does not exist and one belonging to another organization. |
AGENT_RUN_CONCURRENCY_LIMIT | 429 | The organization already has its maximum number of agent runs in flight. Retry after the interval in the Retry-After header. details: { limit, retryAfterSeconds } — the ceiling and when to retry. |
AGENT_RUN_NOT_ACTIVE | 409 | That run has finished, so a message can no longer be sent to it. Send it to the conversation instead to start new work. |
AGENT_SESSION_BUSY | 409 | This conversation already has a run in progress that you did not start. Wait for it to finish, or use a new conversation. |
STALE_APPROVAL | 409 | The rows this approved step would change have changed since it was approved, so it was not run. Read them again and propose it afresh. |
CHANGE_SET_NOT_FOUND | 404 | No recorded change with that handle in your runs. It may belong to someone else, or be older than the retention window. |
Auth-method expansion (services/auth/*)
| Code | Status | Meaning |
|---|---|---|
OAUTH_PROVIDER_UNAVAILABLE | 400 | The sign-in provider is unknown or not configured on this deployment. |
Sessions and tokens
| Code | Status | Meaning |
|---|---|---|
AUTH_TOKEN_EXPIRED | 401 | The access token has expired. Exchange the refresh token for a new pair, then retry. |
AUTH_TOKEN_INVALID | 401 | The access token is not valid. Exchange the refresh token for a new pair, then retry. |
REVERIFY_REQUIRED | 403 | A change that needs a fresh proof it is you (a new email, deleting the account) came without one, or with one that is spent, expired or another session's. Prove it with POST /auth/reverify and send its reverifyToken. details: { methods: Array<'password' | 'totp' | 'sms' | 'email'> }: the proofs this account can give. |
PASSWORD_NOT_SET | 409 | The account has no password yet (it signs in with a provider or a sign-in link). Set one through the password reset email, which proves the address. |
PASSWORD_INCORRECT | 403 | The current password given to change it is not the account's password. |
EMAIL_UNCHANGED | 400 | The new email address is the one the account already has. |
EMAIL_IN_USE | 409 | An email change could not finish: the new address got an account of its own after the change was asked for. |
ACCOUNT_LAST_OWNER | 409 | The account is the last owner or admin of an org that still has other members, so deleting it would leave the org with nobody to manage it. Give another member the owner role, or remove the members, first. details: { orgs: Array<{ uid, name }> } |
ACCOUNT_HAS_SUBSCRIPTION | 409 | The account still has a subscription that is billing it. Cancel it before deleting the account. |
ACCOUNT_DELETE_DISABLED | 403 | The app's owner has not let accounts be deleted by their users. The owner can delete one from the console. |
ACCOUNT_CHANGE_UNAVAILABLE | 400 | This change to an account is available to an app's own users, not to a console account. |
EMAIL_NOT_VERIFIED | 403 | The account has not confirmed its email address yet: it cannot sign in with a password (when the app requires a confirmed address), send invitations or create organizations until it does. Resend the link with POST /auth/verify-email/request. |
ORG_CREATION_DISABLED | 403 | The app's owner has not let its users create organizations beyond their own. |
ORG_LIMIT_REACHED | 409 | The user has created as many organizations as the app allows. |
ACCOUNT_DISABLED | 403 | The app's owner has disabled this account. It cannot sign in until the owner enables it again. |
SESSION_ENDED | 401 | This session has ended. Sign in again. details: { reason: 'expired' | 'revoked' | 'reused' | 'signed_out' | 'invalid' | 'legacy' } |
SESSION_IS_CURRENT | 409 | This is the session making the request. Use sign-out to end it. |
TOKEN_PURPOSE_DENIED | 403 | This token was issued for one purpose and cannot be used for this request. details: { purpose: 'approver' } |
Two-factor authentication
| Code | Status | Meaning |
|---|---|---|
MFA_REQUIRED | 401 | This account has two-factor authentication enabled. Answer the challenge to finish signing in. |
MFA_CODE_INVALID | 400 | The submitted two-factor code did not match. |
MFA_ALREADY_ENROLLED | 409 | A factor of this kind is already set up. Remove it before adding another. |
MFA_NOT_ENROLLED | 400 | This account has no second factor set up. |
MFA_LAST_FACTOR | 409 | This is the only method protecting the account, so it cannot be switched off. |
MFA_ENROLLMENT_REQUIRED | 403 | This organization requires two-factor authentication. Set up a second factor to continue. |
ORG_MFA_ENABLE_BLOCKED | 409 | Set up your own two-factor authentication before requiring it for the organization. |
OAUTH_EMAIL_UNVERIFIED | 400 | The provider returned no verified email address, so the account cannot be matched safely. |
OAUTH_SIGNUP_DISABLED | 400 | No account matches the verified address and sign-up via this provider is disabled. |
OAUTH_EXCHANGE_FAILED | 400 | The token exchange or profile fetch failed at the provider. Usually transient — retry. |
OAUTH_METHOD_DISABLED | 400 | This sign-in method is switched off for the account. Use another method or re-enable it in Settings. |
Product tours
| Code | Status | Meaning |
|---|---|---|
TOUR_NO_SUBJECT | 401 | The request carries no signed-in principal, so there is no tour progress to read or record. |
TOUR_INVALID_KEY | 400 | The tour key must be 1-64 characters of lowercase letters, digits, dot, underscore or hyphen, starting with a letter or digit. |
TOUR_INVALID_VERSION | 400 | The tour version must be a positive integer. |
TOUR_INVALID_STATUS | 400 | The tour status must be either completed or dismissed. |
Console banners & announcements
| Code | Status | Meaning |
|---|---|---|
BANNER_NO_SUBJECT | 401 | Console notices are per person; sign in to read or dismiss them. |
BANNER_INVALID_KEY | 400 | The banner key is not well formed. |
BANNER_NOT_FOUND | 404 | That banner is not currently being shown to you, so it cannot be dismissed. |
BANNER_NOT_DISMISSIBLE | 409 | That notice cannot be dismissed; it goes away when the condition it describes is resolved. |
ANNOUNCEMENT_NO_ORG | 400 | Choose an organization first. |
ANNOUNCEMENT_INVALID | 400 | The announcement is invalid: check severity, title length, the action link, and the dates. |
ANNOUNCEMENT_NOT_FOUND | 404 | No announcement has that uid. |
TOUR_LIMIT_REACHED | 409 | This account has recorded the maximum number of distinct tours; a new one cannot be added. |
Registered 2026-11-04 (VERIFY §3 #12)
| Code | Status | Meaning |
|---|---|---|
ACTOR_CONTEXT_REQUIRED | 401 | This action needs a signed-in caller. |
NO_MEMBER | 401 | This action needs a signed-in member. |
NO_ACTIVE_MEMBER | 403 | No active member is signed in for this organization. |
NO_ACTIVE_ORG | 403 | No active organization is selected. Choose one, then retry. |
ORG_MEMBERSHIP_REQUIRED | 403 | You must be an accepted member of this organization. |
ORG_NOT_FOUND | 404 | The active organization was not found. |
ORG_NAME_REQUIRED | 400 | An organization name is required. |
ORG_NAME_INVALID | 400 | That organization name is not usable. |
MEMBERSHIP_UID_REQUIRED | 400 | Name the membership by its uid (membershipUid). |
MEMBERSHIP_NOT_FOUND | 404 | No membership by that uid in the active organization. |
MEMBERSHIP_SUBJECT_INVALID | 400 | The membership names a subject that cannot be used here. |
LAST_PRIVILEGED_MEMBER_FORBIDDEN | 409 | The organization's last privileged member cannot be removed. |
ORG_OWNER_REMOVAL_FORBIDDEN | 403 | The organization owner's membership cannot be removed. |
OWN_ORG_LEAVE_FORBIDDEN | 403 | You cannot leave your own organization. |
OWN_ORG_SELF_REMOVAL_FORBIDDEN | 403 | You cannot remove yourself from your own organization. |
ORG_OWNER_AMBIGUOUS | 409 | The organization's owner could not be determined unambiguously. |
CANNOT_INVITE_SELF | 400 | You cannot invite yourself. |
INVITEE_EMAIL_REQUIRED | 400 | An invitee email is required. |
INVITEE_EMAIL_ALREADY_EXISTS | 409 | An account with that email already exists. Invite the existing account instead. |
INVITEE_ALREADY_MEMBER | 409 | That person is already a member of this organization. |
INVITEE_SUBJECT_TYPE_MISMATCH | 400 | The invitee's account type must match yours. |
INVITEE_SUBJECT_TYPE_UNSUPPORTED | 400 | That kind of account cannot be invited. |
INVITATION_UID_REQUIRED | 400 | Name the invitation by its uid (invitationUid). |
INVITATION_NOT_FOUND | 404 | No invitation by that uid. |
INVITATION_NOT_PENDING | 409 | That invitation was already accepted, revoked or expired. |
INVITATION_EXPIRED | 410 | That invitation has expired. Ask for a new one. |
INVITATION_TARGET_MISMATCH | 403 | That invitation is addressed to a different account. |
INVITATION_ORG_INVALID | 409 | That invitation has no usable organization. |
INVITATION_ORG_CONTEXT_MISMATCH | 409 | That invitation belongs to a different organization than the active one. |
INVITATION_SUBJECT_TYPE_INVALID | 409 | That invitation is for a kind of account this server does not support. |
INVITED_ACCOUNT_CREATE_FAILED | 500 | The invited account could not be created. Retry, and contact support if it persists. |
INVITED_ACCOUNT_ORG_CREATE_FAILED | 500 | The invited account's own organization could not be created. Retry. |
ORG_INVITE_ONBOARDING_MODE_INVALID | 500 | The server is configured with an unknown invitation onboarding mode. |
ORG_MEMBER_POLICY_MISSING | 500 | The baseline member policy is missing on this server; its migrations need to run. |
APP_OUTSIDE_ORG | 403 | That app is not in your active organization. Switch to the organization that owns it. |
APP_ORG_NOT_FOUND | 404 | No organization by that uid in this app. |
APP_OWNER_ORG_NOT_FOUND | 404 | The organization that owns this app was not found. |
NO_OWNER_TENANT | 404 | This organization has no owner account. |
NO_TENANT | 404 | This organization is not billed. |
NO_STAFF_ORG | 409 | This server has no staff organization yet; finish first-run setup. |
CROSS_PLANE_UNVERIFIED | 403 | Platform code reached another app without passing its access check. |
VALIDATION_ERROR | 400 | The request is not valid; the message says which field and why. |
NOT_FOUND | 404 | Nothing by that uid. |
ROLE_KEY_EXISTS | 409 | A role with that key already exists. |
ROLES_KEY_NAME_REQUIRED | 400 | A role needs a key_name. To set only its display name, use the role actions. |
BASE_ROLE_PROTECTED | 400 | An agent's or job's base role cannot be removed. |
POLICY_NOT_FOUND | 404 | No policy by that uid. |
AGENT_NOT_FOUND | 404 | No agent by that uid in the active organization. |
AGENT_ROLE_MISSING | 500 | The agent role is not seeded in this app; its migrations need to run. |
SELF_DELEGATION | 400 | An agent cannot delegate to itself. |
AGENT_NO_LLM_BINDING | 409 | This agent has no AI connection of its own, and no included AI credits are available to it. |
RUN_NOT_FOUND | 404 | No run by that uid in the active organization. |
RUN_NO_PLAN | 409 | That run has no plan to act on. |
PLAN_NOT_FOUND | 404 | No plan by that uid. |
PLAN_STEP_NOT_FOUND | 404 | No plan step by that uid. |
NO_PENDING_STEPS | 409 | That run has no steps waiting for a decision. |
NOT_A_HANDOFF | 409 | That step did not hand off to an app agent. |
NO_SESSION | 409 | That step has no conversation to hand off to. |
SESSION_NOT_FOUND | 404 | No conversation by that uid. |
APPROVAL_REJECTED | 409 | The approval this call waited on was rejected. |
API_KEY_REVOKED | 409 | A revoked key cannot be rolled. Create a new key instead. |
TOKEN_NOT_FOUND | 404 | No token by that uid. |
CRON_NOT_FOUND | 404 | No scheduled job by that uid in the active organization. |
CRON_RUN_NOT_FOUND | 404 | No run of that job by that uid. |
CRON_SCHEDULE_INVALID | 400 | That schedule is not a valid cron expression. |
CRON_PRINCIPAL_FAILED | 500 | The job's principal could not be created. Retry. |
CRON_PRINCIPAL_MISSING | 409 | This job has no principal to bind a role to. Recreate the job. |
HANDLER_DELEGATION_MISSING | 409 | The endpoint in front of this handler allows no SDK actions. Add the ones it uses to the endpoint's Allowed actions. |
HANDLER_DEPENDENCY_DENIED | 403 | This handler may not call that SDK action for this caller. |
HANDLER_DEPENDENCY_NOT_ALLOWED | 403 | That SDK action is not in the endpoint's Allowed actions. |
HANDLER_NOT_PUBLISHED | 409 | Only a published handler runs live. Publish a version, or use a dry run. |
HANDLER_UNSAFE_RAW_SQL | 400 | sdk.query was given something other than a string literal, or reached indirectly. Use a literal with ? placeholders, or sdk.sql. |
HANDLER_DYNAMIC_MCP_ACCESS | 400 | The handler names an MCP server or tool dynamically. Write sdk.mcp.<alias>.<tool>() literally. |
HANDLER_MCP_TOOL_UNKNOWN | 400 | The handler calls an MCP tool its connection does not have. |
HANDLER_MCP_IN_TRANSACTION | 422 | The handler calls an external MCP tool inside withTransaction. Move the call outside it. |
HANDLER_FILE_PATH_ESCAPE | 422 | A handler file path points outside the handler's own files. |
HANDLER_VENDOR_FAILED | 422 | The handler's remote dependencies could not be vendored at publish. |
HANDLER_VENDOR_UNAVAILABLE | 503 | Remote dependencies cannot be vendored right now. Retry the publish. |
MCP_IN_TRANSACTION | 422 | sdk.mcp cannot be called inside withTransaction: an external call cannot be rolled back. |
SDK_CALL_NOT_AWAITED | 500 | An SDK call was started but never awaited. Add await. |
SDK_CONTRACT_MISSING | 500 | The server's SDK contract file is missing. |
SDK_CRUD_UNAVAILABLE | 500 | The SDK's CRUD helpers are not available in this context. |
SQL_INVALID_IDENTIFIER | 400 | sdk.sql.id() was given a name that is not a valid identifier. |
SQL_UNDEFINED_BINDING | 400 | An sdk.sql template interpolated undefined. Pass null, or a value. |
BULK_WRITE_UNFILTERED | 400 | updateWhere and deleteWhere need a filter, or { all: true } to change every row in scope. |
ROW_VALUE_INVALID | 400 | A value does not fit its column (too long, out of range, the wrong type, or not one of its options); details.column names it. |
READ_LIST_TOO_LARGE | 422 | This read would return more than 10,000 rows. Page through the table with sdk.readPage, or narrow it with filters. |
BULK_LIMIT_EXCEEDED | 400 | Too many rows for one call; split it into smaller batches. |
SOFT_DELETE_COLUMN_MISSING | 500 | A soft delete was asked for on a table without a deleted_at column. |
AGG_SPEC_INVALID | 422 | The aggregate spec is invalid; issues names each problem. |
UPLOAD_SOURCE_UNAVAILABLE | 502 | The URL to upload from could not be fetched. |
ORG_REACH_UNRESOLVED | 500 | The caller's organization in this app could not be resolved. |
AI_CODEGEN_BAD_INPUT | 400 | The handler-generation request is missing what it needs; the message says what. |
AI_CODEGEN_EMPTY_PROMPT | 400 | Describe the handler to generate. |
AI_CODEGEN_EMPTY_RESULT | 502 | The model returned no handler code. Rephrase and try again. |
TABLE_READ_ONLY | 403 | This table is read-only in the data explorer. |
TABLE_HAS_NO_UID | 409 | This table has no uid column, so its rows cannot be addressed one by one. |
TABLE_HAS_DEPENDENTS | 409 | Other tables reference this one; remove those references first. |
REDACTED_PLACEHOLDER_WRITE | 400 | "[redacted]" cannot be saved as a value. Clear the field and type the new one. |
FK_PREFLIGHT_FAILED | 400 | A foreign key in the table definition does not resolve; the message names it. |
PLAN_BLOCKED | 400 | This schema plan has blocking issues and cannot be applied. |
FULL_SCHEMA_REQUIRED_BUT_UNAVAILABLE | 422 | Full schema details were asked for but could not be produced. |
CSV_EMPTY | 400 | The CSV file is empty. |
CSV_PARSE_FAILED | 400 | The CSV could not be parsed; the message says where. |
CSV_TOO_LARGE | 413 | The CSV file is larger than this app allows. |
CSV_TOO_MANY_ROWS | 413 | The CSV has more rows than this app allows in one import. |
CSV_NO_HEADER | 400 | The CSV has no header row. |
CSV_NO_ROWS | 400 | The CSV has a header but no data rows. |
CSV_NO_MAPPED_COLUMNS | 400 | No CSV column maps to a writable column of this table. |
CSV_UNKNOWN_COLUMNS | 400 | Some mapped columns do not exist on the table, or are not writable. |
CSV_UNKNOWN_DEFAULTS | 400 | Some default columns do not exist on the table, or are not writable. |
CSV_INVALID_DEFAULT | 400 | A default value cannot be stored in its column. |
CSV_MISSING_REQUIRED_COLUMNS | 422 | The file does not supply every column the table requires; they are named. |
CSV_ROWS_INVALID | 422 | Some rows hold values the table cannot store. Nothing was written; the errors say where. |
STORAGE_REQUIRED | 400 | Name the storage by its uid (storageUid). |
STORAGE_NOT_FOUND | 404 | No storage by that uid. |
FOLDER_NOT_FOUND | 404 | No folder by that uid. |
FOLDER_RACE_UNRESOLVED | 409 | The folder changed while it was being created. Retry. |
FILE_NOT_FOUND | 404 | No file by that uid. |
FILE_TOO_LARGE | 413 | The file is larger than this storage allows. |
MIME_NOT_ALLOWED | 415 | This storage does not accept that file type. |
INVALID_METADATA | 400 | File metadata must be a JSON object. |
LLM_PROVIDER_UNSUPPORTED | 400 | That AI provider is not supported. |
LLM_PROVIDER_NOT_ENABLED | 403 | That AI provider is not enabled on this server. |
LLM_BASE_URL_REQUIRED | 400 | A self-hosted connection needs a base URL. |
LLM_BASE_URL_INVALID | 400 | That base URL is not a valid address. |
LLM_BASE_URL_NOT_ALLOWED | 400 | This provider does not take a custom endpoint URL. |
LLM_CONNECTION_NO_CREDENTIAL | 409 | The AI connection has no stored credential. |
LLM_CONNECTION_NO_KEY | 409 | The agent's AI connection has no usable key. |
LLM_CONNECTION_UNAVAILABLE | 409 | No AI provider is connected. Connect one in Integrations. |
MCP_ALIAS_IN_USE | 409 | That alias already names another MCP grant in this app. |
MCP_CONNECTION_NOT_FOUND | 404 | No MCP connection by that uid. |
MCP_GRANT_NOT_FOUND | 404 | No MCP grant by that alias. |
NO_BILLING_ACCOUNT | 409 | This organization has no billing account yet. |
NO_ACTIVE_SUBSCRIPTION | 409 | There is no live subscription to change. |
NO_PLAN_LINE_ITEM | 409 | The subscription has no plan line to change. |
PLAN_NOT_PURCHASABLE | 409 | That plan cannot be bought. |
ADDON_NOT_FOUND | 404 | No add-on by that key. |
ADDON_NOT_PURCHASABLE | 409 | That add-on cannot be bought on your plan. |
ADDON_PAYMENT_FAILED | 402 | Your card could not be charged for the add-on, so it was not added. Update your payment method and retry. |
NOT_APP_USER | 403 | Only an app's own users can buy through the app. |
APP_USER_NOT_FOUND | 404 | No app user by that uid. |
SUBSCRIPTION_NOT_FOUND | 404 | No subscription by that uid. |
PRICE_IS_RECURRING | 400 | That price is a subscription; use subscription checkout. |
PRICE_NOT_RECURRING | 400 | That price is one-time; use order checkout. |
PRICE_SCOPE_MISMATCH | 403 | That price does not belong to this app. |
PRODUCT_SCOPE_MISMATCH | 403 | That product does not belong to this app. |
METADATA_TOO_LARGE | 400 | Product metadata is larger than Stripe allows. |
CONNECTION_NOT_FOUND | 404 | Unknown Stripe connection. |
NO_STRIPE_CONNECTION | 409 | No Stripe account is connected in that mode. Connect one first. |
STRIPE_KEY_INVALID | 400 | Stripe rejected that key. |
STRIPE_PACKAGE_MISSING | 500 | The server's Stripe library is missing. |
WEBHOOK_SECRET_MISSING | 400 | The Stripe connection has no webhook secret. |
WEBHOOK_PROVISION_FAILED | 502 | Stripe would not set up the webhook; the message says why. |