This page documents how the Doxbrix API reports errors and how rate limiting works, so your integration can handle failures gracefully and back off correctly.
This page documents how the Doxbrix API reports errors and how rate limiting works, so your integration can handle failures gracefully and back off correctly.
This page documents how the Doxbrix API reports errors and how rate limiting works, so your integration can handle failures gracefully and back off correctly.
Error format
Errors return a consistent JSON object with a machine-readable error code, a human-readable message, and (where relevant) the offending field:
{
"error": "validation_error",
"message": "The 'title' field is required.",
"field": "title",
"requestId": "req_01J8X9Z3K2M4N5"
}Always log the requestId — include it when contacting support about a specific failed request.
HTTP status codes
| Status | Meaning |
|---|---|
| `200 OK` | Request succeeded |
| `201 Created` | Resource created |
| `204 No Content` | Succeeded with no body (e.g. delete) |
| `400 Bad Request` | Malformed request or validation error |
| `401 Unauthorized` | Missing or invalid API key |
| `403 Forbidden` | Authenticated, but not permitted (scope/project) |
| `404 Not Found` | Resource doesn't exist |
| `409 Conflict` | Conflicts with current state (e.g. duplicate slug) |
| `422 Unprocessable Entity` | Semantically invalid (e.g. publishing an empty page) |
| `429 Too Many Requests` | Rate limit exceeded |
| `5xx Server Error` | Something went wrong on our side |
Error codes
The error field is a stable string you can branch on:
| Code | Status | Meaning |
|---|---|---|
| `validation_error` | 400 | A field failed validation (see `field`) |
| `unauthorized` | 401 | No/invalid API key |
| `forbidden` | 403 | Key lacks the required scope or project access |
| `not_found` | 404 | Resource not found |
| `duplicate_slug` | 409 | A page/space with that slug already exists |
| `not_publishable` | 422 | Content failed publish validation |
| `rate_limited` | 429 | Too many requests |
| `internal_error` | 500 | Unexpected server error |
Handling errors
Branch on the error code, not the message (messages may change):
try {
await dx.pages.create({ title: '', spaceId: 'sp_01' })
} catch (e) {
if (e.error === 'validation_error') {
console.error(`Invalid field: ${e.field}`)
} else if (e.error === 'rate_limited') {
await sleep(e.retryAfter * 1000)
} else {
throw e
}
}Rate limits
API requests are rate-limited per API key. Limits depend on your plan:
| Plan | Requests / minute |
|---|---|
| Free | 60 |
| Pro | 600 |
| Business | 1,200 |
| Enterprise | Custom |
Rate limit headers
Every response includes your current rate-limit state:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 583
X-RateLimit-Reset: 1700000000| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | Your per-minute ceiling |
| `X-RateLimit-Remaining` | Requests left in the window |
| `X-RateLimit-Reset` | Unix time when the window resets |
When you hit the limit
A 429 response includes a Retry-After header (seconds) telling you when to retry:
{
"error": "rate_limited",
"message": "Rate limit exceeded. Retry after 12 seconds.",
"retryAfter": 12
}Backing off
Implement exponential backoff with jitter on 429 and 5xx responses:
On 429, wait the number of seconds in Retry-After before retrying.
Retry transient 5xx errors with increasing delays (1s, 2s, 4s, …) plus jitter.
Give up after a few attempts and surface the error with its requestId.
Idempotency and conflicts
For POST requests, an Idempotency-Key header lets you retry safely after a network error without creating duplicates. A 409 duplicate_slug means the resource already exists — fetch it rather than retrying blindly.