Error Handling
The API uses standard HTTP status codes with structured JSON error responses. Every error includes a code for programmatic handling and a human-readable message.
Error Response Format
All error responses follow this structure:
{
"error": {
"code": "not_found",
"message": "Party with ID 'party_invalid' not found.",
"details": {
// Optional additional context
}
},
"meta": {
"requestId": "req_abc123xyz"
}
}Always include the requestId when contacting support—it lets us trace the exact request through our systems.
Error Codes
bad_requestRequest body or query is malformed. `error.details` includes Zod issues when validation failed.
Resolution: Check the request shape against the OpenAPI spec (/openapi.json). Fix the offending field.
unauthorizedMissing, malformed, or revoked Authorization header.
Resolution: Send `Authorization: Bearer ak_live_…` (or `ak_test_…`). Mint a fresh key if yours was revoked.
unsupported_auth_schemeAuthorization header used a scheme we don't accept (e.g. Basic).
Resolution: Use Bearer with an adaptlive API key.
forbiddenValid credentials, but either your org isn't enabled for /api/v1 or your key's scope is too narrow for this operation.
Resolution: Confirm the key's scope (READ ⊂ WRITE ⊂ ADMIN). For dev-portal enablement, email developers@adaptlive.app.
not_foundNo record matches the given identifier — either it doesn't exist or it belongs to a different organization.
Resolution: Verify the id and that the key has access to its parent org.
idempotency_conflictAn idempotency key was reused with a different request body. The API refuses to replay because the inputs disagree.
Resolution: Drop the idempotency key, or replay with the exact original body.
rate_limitedThrottled. Honor the Retry-After header on the first retry; back off with jitter after that.
Resolution: See the Authentication → Rate limits section for headers + recommended retry strategy.
internal_errorUnexpected server error. Always logged with the requestId.
Resolution: Retry once. If it persists, email developers@adaptlive.app with the `meta.requestId` from the response.
HTTP Status Codes
| Range | Meaning | Action |
|---|---|---|
| 2xx | Success | Request completed successfully. |
| 4xx | Client error | Fix the request before retrying. |
| 5xx | Server error | Safe to retry with exponential backoff. |
Retry Strategy
For transient errors (429, 5xx), implement exponential backoff with jitter:
async function fetchWithRetry(url, options, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options);
if (res.status === 429) {
// Out of retries — surface the throttle to the caller instead
// of falling out of the loop with no return value.
if (attempt === maxRetries) {
throw new Error(`Rate limited after ${maxRetries + 1} attempts`);
}
const retryAfter = res.headers.get("Retry-After") || "1";
await sleep(parseInt(retryAfter, 10) * 1000);
continue;
}
if (res.status >= 500) {
throw new Error(`Server error: ${res.status}`);
}
return res;
} catch (err) {
lastError = err;
if (attempt === maxRetries) throw err;
// Exponential backoff with jitter
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
await sleep(baseDelay + jitter);
}
}
// Belt-and-suspenders: the loop above always exits via return or
// throw, but keep this so the function's contract ("returns a
// Response or throws") holds even if someone tweaks the loop later.
throw lastError ?? new Error("fetchWithRetry exhausted without a response");
}Validation Errors
When request validation fails, the error includes field-level details:
{
"error": {
"code": "bad_request",
"message": "Validation failed.",
"details": {
"fields": [
{
"field": "email",
"message": "Must be a valid email address."
},
{
"field": "scheduledEnd",
"message": "Must be after scheduledStart."
}
]
}
},
"meta": {
"requestId": "req_abc123"
}
}Common Issues
- "unauthorized" even with a valid-looking key
- Check the key hasn't been revoked. Keys are organization-scoped—ensure you're using a key from the correct organization.
- "forbidden" on write operations
- Your API key may have READ-only scope. Mint a new key with WRITE scope from /portal/api-keys.
- "not_found" for a record you just created
- Records are organization-scoped. Verify the API key belongs to the same organization that owns the record.
- Unexpected 500 errors
- Include the
requestIdwhen contacting developers@adaptlive.app.
