Errors
Read one error envelope everywhere, branch on its code, and know how each door reports it.
Every failed request answers with the same body. Branch on code; fall back to statusCode for coarse categories. Keep the x-request-id header when you report a problem.
The error envelope
A lookup of a missing record returns:
{
"success": false,
"statusCode": 404,
"message": "Record \"abc123\" not found.",
"code": "SCHEMA_RECORD_NOT_FOUND"
}| Field | Description |
|---|---|
success | Always false for errors. |
statusCode | The HTTP status, mirrored in the body. |
message | A human-readable explanation. Every 5xx message is masked to Internal server error. |
code | A stable machine-readable code. Absent when the request body failed validation. |
When request-body parsing fails, message is Validation failed, no code is set, and an errors array points at each invalid field. path is an array because it can point into nested fields:
{
"success": false,
"statusCode": 400,
"message": "Validation failed",
"errors": [{ "path": ["limit"], "message": "Expected number, received string" }]
}A value that fails its attribute's type or constraints is a different case: it answers SCHEMA_VALIDATION_FAILED, with a code.
Request ids
Every response, success or error, carries an x-request-id header holding a UUID that identifies that exact request on the server side:
x-request-id: 4b2f1c9e-8a3d-4f6b-9c1e-2d7a5e8b0c4fIt is the only response header the API promises. Include it when reporting an issue.
Error codes
The codes you can meet from the SDK, the CLI and MCP:
| Code | HTTP | Meaning |
|---|---|---|
SCHEMA_VALIDATION_FAILED | 400 | Input failed schema validation. Also refuses revoking the last Owner credential. |
SCHEMA_INVALID_OBJECT_NAME | 400 | The object name is malformed. |
SCHEMA_RESERVED_OBJECT_NAME | 400 | The object name belongs to a framework-native object (skill, memory, drives, artifact, meeting). |
SCHEMA_INVALID_ATTRIBUTE_NAME | 400 | An attribute name is malformed. |
DOCUMENT_MIME_NOT_ACCEPTED | 400 | The file matches no entry of the document attribute's accepts list. |
DOCUMENT_MULTIPLE_NOT_ALLOWED | 400 | The document attribute takes a single file. |
DOCUMENT_FILE_TOO_LARGE | 400 | The file exceeds the document attribute's maxFileSize. |
SCHEMA_FORBIDDEN | 403 | The actor lacks permission. The message never names what was refused. |
SCHEMA_ACTOR_CONTEXT_REQUIRED | 403 | No authenticated actor reached the handler. |
SCHEMA_PROTECTED_OBJECT | 403 | The object is system-protected. |
SCHEMA_PROTECTED_ATTRIBUTE | 403 | The attribute is system-protected. |
SCHEMA_SYSTEM_ENTITY_IMMUTABLE | 403 | A system entity, such as a built-in role, cannot be modified. |
SCHEMA_WORKSPACE_INVARIANT | 403 | A workspace rule refuses the write. The message names the remedy. |
SCHEMA_RECORD_NOT_FOUND | 404 | No record with that id. |
SCHEMA_OBJECT_NOT_FOUND | 404 | No object with that name. |
SCHEMA_ATTRIBUTE_NOT_FOUND | 404 | No such attribute. |
SCHEMA_SOURCE_NOT_FOUND | 404 | No schema source with that id. |
SCHEMA_ROLE_NOT_FOUND | 404 | No such role. |
DOCUMENT_NOT_FOUND_FOR_OPERATION | 404 | The target document does not exist. |
SCHEMA_TIMEOUT | 408 | The operation timed out. Retry after backing off. |
SCHEMA_DUPLICATE_OBJECT | 409 | An object with that name already exists, or is owned by core, a bundle or another source. |
SCHEMA_DUPLICATE_ATTRIBUTE | 409 | An attribute with that name already exists. |
SCHEMA_DUPLICATE | 409 | A view with that name is owned by another source. |
SCHEMA_CONFLICT | 409 | The request conflicts with current state. |
SCHEMA_OBJECT_REFERENCED | 409 | The object is still the target of relations on other objects. Drop those relation attributes first. |
SCHEMA_SYNC_CONFLICT | 409 | A declaration cannot be reconciled with what the workspace already holds, such as a runtime attribute whose type clashes with the code. Relax the constraint, migrate the values, or rename the declaration. |
SCHEMA_SYNC_FAILED | 500 | A sync stopped partway. The message is masked; use the request id. |
SCHEMA_DESTRUCTIVE_NOT_ALLOWED | 500 | A sync would drop data and no opt-in allows it. The message is masked; use the request id. |
SCHEMA_ACCESS_DENIED | 500 | Deleting a file someone else uploaded. The message is masked. |
CONFIGURATION_REQUIRED | 503 | An optional capability is not wired on this deployment. Permanent: do not retry. |
The HTTP status derives from the code. A code without an explicit mapping answers 500.
SCHEMA_FORBIDDEN is deliberately vague
A 403 carrying SCHEMA_FORBIDDEN always reads You do not have permission to perform this action. The envelope never names the object or the action, because the message reaches end users who have no access to those names. Branch on the code, not on the sentence. The other 403 codes are refusals a permission grant would not lift, so each keeps a message naming the remedy.
Rate limits
The hosted API counts requests per client IP in five buckets, each per minute:
| Bucket | Routes | Per minute |
|---|---|---|
| Default | Records, schema, keys, and every route not listed below. A per-object record search is a list read and counts here. | 100 |
| Upload | File uploads (POST under /files, and /upload routes). | 20 |
| Global search | /search and /global-search, cross-object queries. | 30 |
| Bulk | bulk-create, bulk-update, bulk-delete. | 10 |
| AI | Agent chat and streaming. | 5 |
Pushing a schema source (PUT /schema/sources/:id) is exempt: a CI run carries a whole schema in one call.
Exceeding a limit returns a 429 in the same envelope, without a code:
{ "success": false, "statusCode": 429, "message": "ThrottlerException: Too Many Requests" }No rate-limit or retry-after header is promised. x-request-id is the only guaranteed header. Treat a 429 as transient: back off and retry.
How each door surfaces an error
| Door | Shape | Notes |
|---|---|---|
| SDK | StandardsRequestError with status and code; StandardsAuthError for 401 and 403. | status: 0 means a timeout or a refused connection, message Could not connect to <baseUrl>. The errors[] array and the x-request-id header are dropped; only message and code survive. |
| CLI | ✗ Error (status): message on stderr, exit code 1. | exit code 2 when no instance is configured: ✗ Error: No Standards instance configured. Run "standards login" or pass --api-key. |
| MCP | JSON-RPC -32602 for an unknown tool, or a tool your key or scopes withheld. | Inside a call, the result carries isError: true and a text content Access denied: not authorized to <action> "<object>". |
import { createStandards, StandardsRequestError } from "@stndrds/client";
const standards = createStandards({
baseUrl: "https://api.standards.new/v1",
apiKey: process.env.STANDARDS_API_KEY!,
});
try {
await standards.from("contacts").get("abc123");
} catch (error) {
if (error instanceof StandardsRequestError && error.status === 0) {
// Timeout or refused connection: the API was never reached.
}
if (error instanceof StandardsRequestError && error.code === "SCHEMA_RECORD_NOT_FOUND") {
// Branch on the code, not on the message.
}
throw error;
}Handling errors
Branch on code for precise handling. Fall back to statusCode for categories: 4xx means the request is wrong, so fix it before retrying; 5xx is server-side and safe to retry with backoff. 408 and 429 are safe to retry as-is after backing off. CONFIGURATION_REQUIRED is the one 5xx not worth retrying: the capability is absent from the deployment, and the answer will not change.
Next steps
- SDK errors: the error classes,
SchemaSourceConflictError, and what a sync reports. - Workspaces and API keys: why a
403says nothing and how to grant the missing action. - CLI: exit codes, output formats and the
--instanceflag. - MCP: how an agent sees a withheld tool and a refused call.