All Fenicia API errors follow a consistent JSON shape. This page is the canonical reference for every error code the Orders API may return.
{
"code": "INVALID_API_KEY",
"message": "Invalid or expired API key"
}Some errors include additional fields (e.g. retryAfter on rate-limit responses, or details on validation errors).
{
"code": "validation:invalid_format",
"message": "Field 'customerInfo.email' has an invalid format",
"details": { "field": "customerInfo.email", "expected": "email" }
}| Code | Description |
|---|---|
MISSING_AUTHORIZATION | The Authorization header was not sent |
INVALID_AUTHORIZATION_FORMAT | Header does not follow Bearer <key> format |
INVALID_API_KEY | API key does not exist, is expired, or has been revoked |
{ "code": "INVALID_API_KEY", "message": "Invalid or expired API key" }| Code | Description |
|---|---|
INSUFFICIENT_PERMISSIONS | The API key lacks the required scope for this endpoint |
account/billing_restricted | Tenant is suspended due to billing issues |
{
"code": "INSUFFICIENT_PERMISSIONS",
"message": "API key is missing required scope: orders:create"
}| Code | Description |
|---|---|
validation:missing_field | A required field was not provided |
validation:invalid_format | Field has a wrong format (email, date, UUID, etc.) |
validation:invalid_value | Value not allowed (invalid enum, out of range) |
{
"code": "validation:missing_field",
"message": "Field 'customerInfo.email' is required",
"details": { "field": "customerInfo.email" }
}| Code | Description |
|---|---|
order_not_found | No order exists with the given ID for this tenant |
return_not_found | No return exists with the given ID |
attachment_not_found | No attachment exists with the given ID |
{ "code": "order_not_found", "message": "Order ord_123 not found" }| Code | Description |
|---|---|
invalid_transition | Cannot transition to the target status from the current one |
order_already_cancelled | The order is already in cancelled state |
insufficient_inventory | Not enough stock to fulfill the order |
{
"code": "invalid_transition",
"message": "Cannot transition from 'cancelled' to 'fulfilled'"
}| Code | Description |
|---|---|
RATE_LIMIT_EXCEEDED | Request rate exceeded. Inspect retryAfter (seconds) to know when to try again |
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests",
"retryAfter": 45
}Advertencia
Always respect retryAfter. Retrying sooner will keep you locked out and may trigger longer cooldowns.
| Code | Status | Description |
|---|---|---|
INTERNAL_ERROR | 500 | Generic server error. Transient, safe to retry with backoff |
EXTERNAL_SERVICE_UNAVAILABLE | 503 | Downstream channel or payment provider is unavailable |
{
"code": "EXTERNAL_SERVICE_UNAVAILABLE",
"message": "Shopify API is currently unavailable"
}For transient errors (500, 503, network timeouts), retry with exponential backoff — doubling the delay on each attempt.
async function requestWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status === 429) {
const { retryAfter = 1 } = await res.json();
await sleep(retryAfter * 1000);
continue;
}
if (res.status >= 500) {
const delay = Math.min(1000 * 2 ** attempt, 30_000);
await sleep(delay);
continue;
}
return res;
}
throw new Error("Max retries exceeded");
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));| Status | Retry? | Strategy |
|---|---|---|
400 validation | No | Fix the request, surface error to caller |
401 auth | No | Rotate key or re-authenticate |
403 permission | No | Request a scope upgrade |
404 not found | No | The resource does not exist |
409 business logic | Sometimes | Only if you can mutate state to resolve conflict |
429 rate limit | Yes | Wait retryAfter then retry |
500 internal | Yes | Exponential backoff, up to ~5 attempts |
503 external down | Yes | Longer backoff — dependency is recovering |
Mutating endpoints (POST, PUT, DELETE) accept an Idempotency-Key header. Use it when retrying to avoid creating duplicate resources:
curl -X POST https://api.fenicia.io/orders \
-H "Authorization: Bearer fn_live_your_api_key" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{...}'Tip
Generate a fresh idempotency key per logical operation, but reuse the same key across retries of that same operation. Store it alongside the operation until you see a successful response.
code, not the messageError messages may be translated or refined over time. Error codes are stable — branch on code in your integration, not on message text.
// Good
if (error.code === "insufficient_inventory") {
await notifyMerchant(order);
}
// Fragile
if (error.message.includes("stock")) { ... }support@fenicia.io