All API errors return a consistent JSON envelope:
"message": "Human-readable description",
code — Machine-readable error identifier (use this for programmatic handling)
message — Human-readable description (may change, don’t parse this)
retryable — Whether the request can be retried as-is
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
unauthorized | 401 | No | Missing, invalid, or expired bearer token | Check your API key is correct and active. Keys use the brk_ prefix. |
The API returns WWW-Authenticate: Bearer realm="brightly" on all 401 responses.
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
insufficient_scope | 403 | No | API key doesn’t have the required scope | Create a new key with the needed scope, or update the existing key’s scopes. |
session_mismatch | 403 | No | MCP session belongs to a different API key | Start a new MCP session with the correct API key. |
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
validation_error | 400 | No | Request body or query params failed Zod validation | Check the error message for the specific field that failed. Review the endpoint’s request body documentation. |
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
not_found | 404 | No | Resource doesn’t exist or isn’t in your org | Verify the UUID is correct and belongs to your organization. |
lead_not_found | 404 | No | Lead referenced in appointment creation doesn’t exist in your org | Check the lead_id exists via GET /api/v1/leads/:id first. |
duplicate | 409 | No | Record violates a unique constraint | A lead with this phone/email may already exist. Check before creating. |
compliance_blocked | 422 | No | Message blocked by compliance checks | The lead has sms_opted_out: true or on_dnc_list: true. Check these fields before sending. |
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
rate_limit_exceeded | 429 | Yes | Too many requests in the current window | Wait and retry. Reads: 60/min. Writes: 30/min. Use exponential backoff with jitter. |
Every response includes rate limit information:
RateLimit-Reset: 1715385600
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
query_failed | 500 | Yes | Database read failed | Transient error — retry the request. If persistent, contact support. |
create_failed | 500 | Yes | Database write failed | Transient error — retry with the same request body. |
internal_error | 500 | Yes | Unhandled server error | Retry the request. If persistent, contact support. |
| Code | HTTP Status | Retryable | Description | How to Fix |
|---|
no_session | 400 | No | GET request without an active MCP session | Send a POST to initialize a session first, then include the mcp-session-id header. |
too_many_sessions | 503 | Yes | Server at 1,000 concurrent session capacity | Wait for existing sessions to expire (30-min TTL) or close unused sessions with DELETE /mcp. |
For errors with retryable: true, use exponential backoff with jitter:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const res = await fetch(url, options);
if (res.status !== 429 && res.status < 500) return res;
const delay = Math.min(1000 * Math.pow(2, i), 30000);
const jitter = delay * (0.5 + Math.random() * 0.5);
await new Promise(r => setTimeout(r, jitter));
throw new Error('Max retries exceeded');