Errors
Understand API error responses and how to handle them
When an API request fails, Coherence returns a structured error response with details to help you diagnose and resolve the issue.
Error Response Format
All error responses follow a consistent envelope:
{
"error": {
"code": "not_found",
"message": "Record not found",
"statusCode": 404
}
}Validation failures include an issues array describing each failed field:
{
"error": {
"code": "validation_error",
"message": "Validation failed",
"statusCode": 400,
"issues": [
{ "path": ["email"], "message": "Invalid email" }
]
}
}Error Object Properties
| Property | Type | Description |
|---|---|---|
code | string | Stable machine-readable error code (see the catalog below) |
message | string | Human-readable error description |
statusCode | number | HTTP status code (mirrors the response status) |
issues | array | Present on validation errors (code: "validation_error") — one entry per failed field |
data | object | Additional context on some errors (optional) |
Branch on code for error handling — it is a stable, machine-readable string.
statusCode mirrors the HTTP status and remains available; message is for
humans and may change wording over time.
Error Codes
Every error response carries a code from this catalog:
| Code | HTTP Status | Meaning | Retry? |
|---|---|---|---|
bad_request | 400 | Malformed or invalid request | No — fix the request |
validation_error | 400 | Request failed schema validation; inspect issues | No — fix the request |
unauthorized | 401 | Authentication failed or credentials missing | No — fix credentials |
forbidden | 403 | Valid credentials but insufficient permissions or scope | No — fix permissions |
not_found | 404 | The requested resource does not exist | No |
conflict | 409 | The request conflicts with existing data | No — resolve the conflict |
unprocessable_entity | 422 | Request syntax is valid but semantically incorrect | No — fix the request |
rate_limit_exceeded | 429 | Rate limit exceeded | Yes — wait Retry-After seconds |
internal_error | 500 | An unexpected error occurred on our servers | Yes — exponential backoff |
bad_gateway | 502 | Temporary network issue | Yes — exponential backoff |
service_unavailable | 503 | Service temporarily unavailable | Yes — exponential backoff |
gateway_timeout | 504 | Request timed out | Yes — exponential backoff |
Any other 4xx status maps to bad_request and any other 5xx maps to
internal_error. The catalog is additive-only: codes are never renamed or
removed, but individual endpoints may emit new, more specific codes over time —
treat unknown codes as you would their statusCode class.
HTTP Status Codes
Coherence uses standard HTTP status codes to indicate the result of API requests.
Client Errors (4xx)
| Status | Name | Description |
|---|---|---|
| 400 | Bad Request | The request was malformed or contains invalid data |
| 401 | Unauthorized | Authentication failed or credentials missing |
| 403 | Forbidden | Valid credentials but insufficient permissions |
| 404 | Not Found | The requested resource does not exist |
| 409 | Conflict | The request conflicts with existing data |
| 422 | Unprocessable Entity | Request syntax is valid but semantically incorrect |
| 429 | Too Many Requests | Rate limit exceeded |
Server Errors (5xx)
| Status | Name | Description |
|---|---|---|
| 500 | Internal Server Error | An unexpected error occurred on our servers |
| 502 | Bad Gateway | Temporary network issue |
| 503 | Service Unavailable | Service temporarily unavailable |
| 504 | Gateway Timeout | Request timed out |
Server errors (5xx) are rare and typically transient. If you encounter them persistently, check our status page or contact support.
Handling Errors
Branch on the machine-readable code field, or on the HTTP status (the statusCode field mirrors it):
const response = await fetch(
'https://api.getcoherence.io/v1/modules/contacts/records',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ fields: { name: 'John Smith' } })
}
);
if (!response.ok) {
const body = await response.json().catch(() => null);
const statusCode = body?.error?.statusCode ?? response.status;
const code = body?.error?.code;
if (code === 'validation_error') {
// Inspect the issues array
console.error('Validation failed:', body?.error?.issues);
return;
}
switch (statusCode) {
case 400:
console.error('Bad request:', body?.error?.message);
break;
case 401:
console.error('Check your API key:', body?.error?.message);
break;
case 403:
console.error('Missing permission or scope:', body?.error?.message);
break;
case 404:
console.error('Resource not found:', body?.error?.message);
break;
case 429: {
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
console.warn(`Rate limited. Retry in ${retryAfter}s`);
break;
}
default:
console.error(`API error ${statusCode}:`, body?.error?.message);
}
}Validation Errors (400)
When request validation fails, the envelope carries code: "validation_error" and the issues array lists every problem. Each entry has a path (the location of the invalid value) and a message, along with additional context fields describing the specific check that failed:
{
"error": {
"code": "validation_error",
"message": "Validation failed",
"statusCode": 400,
"issues": [
{
"code": "too_big",
"maximum": 100,
"type": "number",
"inclusive": true,
"path": ["pageSize"],
"message": "Number must be less than or equal to 100"
},
{
"code": "invalid_enum_value",
"options": ["asc", "desc"],
"received": "up",
"path": ["sortDirection"],
"message": "Invalid enum value. Expected 'asc' | 'desc', received 'up'"
}
]
}
}Use path to map each issue back to the offending parameter or body field, and surface message to your users or logs. Fix the request before retrying — validation errors are never transient.
Rate Limit Errors (429)
429 responses include a Retry-After header (in seconds) alongside the standard envelope:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706745600
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after the window resets.",
"statusCode": 429
}
}Wait at least Retry-After seconds before retrying. See Rate Limiting & Best Practices for backoff patterns and header details.
Retry Guidance
Not every error should be retried:
| Status | Retry? | Approach |
|---|---|---|
| 400, 401, 403, 404, 409, 422 | No | Fix the request, credentials, or permissions first |
| 429 | Yes | Wait for Retry-After seconds, then retry |
| 500, 502, 503, 504 | Yes | Retry with exponential backoff and jitter |
GET requests are always safe to retry. Be careful with mutations (POST, PATCH, DELETE): if a request failed after the server may have processed it (a timeout or 5xx), blindly retrying can create duplicates — check whether the operation actually succeeded before resending.
async function withRetry<T>(
fn: () => Promise<Response>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<Response> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fn();
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
await sleep(retryAfter * 1000);
continue;
}
if (response.status >= 500) {
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
await sleep(delay);
continue;
}
return response; // Success or a non-retryable client error
}
throw new Error('Max retries exceeded');
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}For programmatic retry logic, the code field is the most reliable signal:
retry only on rate_limit_exceeded, internal_error, bad_gateway,
service_unavailable, and gateway_timeout.
Related: API Overview | Authentication | Rate Limiting & Best Practices