Guides
Error Handling
Understand error responses, HTTP status codes, and retry strategies.
Error response format
All errors return a consistent JSON structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Human-readable error description",
"details": [
{ "field": "content", "message": "Content is required" }
]
}
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable description |
details | array? | Per-field validation errors (only for 422) |
HTTP status codes
| Status | Code | Description |
|---|---|---|
401 | UNAUTHORIZED | Missing, invalid, or expired API key |
403 | FORBIDDEN | Key doesn't have the required permission (e.g. read-only key on a write endpoint) |
404 | NOT_FOUND | Resource doesn't exist or doesn't belong to you |
422 | VALIDATION_ERROR | Invalid request body — check details array |
429 | RATE_LIMITED | Too many requests — see Rate Limits |
500 | INTERNAL_ERROR | Unexpected server error |
Error codes reference
| Code | Meaning |
|---|---|
UNAUTHORIZED | API key is missing, malformed, expired, or revoked |
FORBIDDEN | Permission denied for this operation |
NOT_FOUND | The requested resource was not found |
VALIDATION_ERROR | Request body failed validation |
RATE_LIMITED | Rate limit exceeded for this API key |
INTERNAL_ERROR | An unexpected error occurred on our end |
Handling errors
const res = await fetch('https://app.feedhorizon.dev/api/v1/posts', {
method: 'POST',
headers: {
'Authorization': 'Bearer fh_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: 'Hello!' }),
});
if (!res.ok) {
const { error } = await res.json();
console.error(`[${error.code}] ${error.message}`);
if (error.details) {
error.details.forEach(d =>
console.error(` ${d.field}: ${d.message}`)
);
}
}Retry strategy
| Status | Retry? | Strategy |
|---|---|---|
401 | No | Fix your API key |
403 | No | Use a key with write permissions |
404 | No | Check the resource ID |
422 | No | Fix the request body |
429 | Yes | Wait for X-RateLimit-Reset header (seconds until reset) |
500 | Yes | Exponential backoff (1s, 2s, 4s, max 30s) |
For 429 responses, use the rate limit headers:
if (res.status === 429) {
const resetIn = parseInt(res.headers.get('X-RateLimit-Reset') || '60');
await new Promise(resolve => setTimeout(resolve, resetIn * 1000));
// Retry the request
}