Errors
Every error uses the same envelope. The HTTP status is authoritative; the code field lets you branch on specific conditions without parsing English text.
Envelope
{
"error": {
"type": "invalid_request_error" | "permission_denied" | "rate_limit_error" | "server_error",
"code": "VALIDATION_ERROR",
"message": "Human-readable summary of the first issue",
"param": "email", // optional: which field failed
"issues": [ // optional: full field-level list
{ "field": "email", "message": "Invalid email" }
]
},
"meta": {
"request_id": "b28f10a3-..." // include when contacting support
}
}HTTP status mapping
- 2xx. success.
- 400. validation error, malformed body or query.
- 401. missing or invalid API key.
- 403. key valid but lacks scope, or tenant hit PAYG limit.
- 404. resource not found (or belongs to another tenant. response is identical to avoid enumeration).
- 405. endpoint doesn't support this method.
- 409. Idempotency-Key reused with different body.
- 429. rate-limited or auth-throttled.
- 5xx. server-side. Include the
request_idin support tickets so we can trace.
All error codes
| Code | HTTP | Meaning |
|---|---|---|
| INVALID_API_KEY | 401 | Missing, invalid, revoked or expired API key |
| AUTH_RATE_LIMITED | 429 | Too many failed auth attempts from this IP |
| SCOPE_INSUFFICIENT | 403 | API key does not grant the required scope |
| VALIDATION_ERROR | 400 | Request body/query failed Zod validation (see issues array) |
| RESOURCE_NOT_FOUND | 404 | Resource id does not exist or belongs to another tenant |
| METHOD_NOT_ALLOWED | 405 | Endpoint does not support this HTTP method |
| INVALID_STATE | 422 | Resource is in a state that does not allow this operation (e.g. sent quote cannot be edited) |
| PAYG_LIMIT | 403 | Free plan resource limit reached (upgrade required) |
| IDEMPOTENCY_KEY_TOO_LONG | 400 | Idempotency-Key header exceeds 128 chars |
| IDEMPOTENCY_KEY_CONFLICT | 409 | Same Idempotency-Key used with different body |
| IDEMPOTENCY_KEY_QUOTA_EXCEEDED | 429 | Too many unique Idempotency-Keys today |
| RATE_LIMITED | 429 | Request rate exceeded (per-key or global backstop) |
| REQUEST_TOO_LARGE | 413 | Request body exceeds 1 MB (Workspace API cap) |
| MIDDLEWARE_ORDER | 500 | Server misconfiguration. report to support |
| INTERNAL | 500 | Unexpected server error. check request_id in support ticket |
Soft-delete + recycle bin
DELETE endpoints use a 30-day recycle bin. Resources are hidden from default list queries but recoverable via POST /:id/restore. After 30 days a daily purge cron hard-deletes the row for good (including nullifying FK links and purging attachments).
Requires a separate .delete scope — not covered by .write. This makes destructive access an explicit grant per key. See Scopes.
Request-ID for support
Every response includes an X-Request-Id header matching the meta.request_id field. When opening a support ticket, always include this. we use it to look up the exact log line.