Error Catalog

This page is the canonical reference for all error codes the Orders API can return. Codes are grouped by namespace (orders:, returns:, validation:, attachments:, channels:/service:, auth:, abandoned_carts:, sync:, internal:, route:), which corresponds to the domain that originated the error, not the endpoint that returned it — the same code can appear on several different endpoints.

Shape of the error response

Every error response in this domain has exactly this shape:

{
  "error": {
    "code": "orders:not_found",
    "message": "Order not found",
    "details": {
      "field": "orderId",
      "value": "65f3a1b2c4d5e6f7a8b9c0d1"
    }
  }
}
FieldTypeDescription
error.codestringStable identifier, namespace:snake_case format. Branch your logic on this field.
error.messagestringHuman-readable description meant for logs/debugging. Its text can change between versions — never parse it.
error.detailsobject (optional)Additional context. May carry field, value, allowedValues, or other keys depending on the error.

The code is never SCREAMING_SNAKE_CASE

Every code follows the lowercase namespace:snake_case format (orders:not_found, returns:already_approved). If you see an uppercase, namespace-less code in some outdated example, it is not a real code from this API.

How to handle an error

const response = await fetch("https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1", {
  headers: { "Authorization": `Bearer ${process.env.FENICIA_API_KEY}` },
});
 
const body = await response.json();
 
if (!response.ok) {
  switch (body.error.code) {
    case "orders:not_found":
      // the order doesn't exist in your tenant
      break;
    case "auth:permission_denied":
      // your API key doesn't have the required scope
      break;
    default:
      // handle the rest by HTTP status + code
      console.error(body.error.code, body.error.message);
  }
}

orders: — order lifecycle and persistence

CodeStatusWhen it occurs
orders:not_found404No order with that ID exists in your tenant.
orders:duplicate409An order with the same tenant + externalId + channel combination already exists.
orders:invalid_transition422The requested state transition is not valid from the current state. See State Transitions.
orders:already_accepted409The order was already accepted previously.
orders:already_fulfilled409The order was already marked as fulfilled.
orders:already_cancelled409The order was already cancelled.
orders:cannot_cancel422The order's current state doesn't allow cancelling it.
orders:cannot_fulfill422The order's current state doesn't allow fulfilling it.
orders:cannot_confirm_delivery422The order is in a non-deliverable state; its delivery cannot be confirmed.
orders:channel_unsupported422The order's channel doesn't support the requested operation.
orders:settlement_query_invalid400The date window of a settlement query (settlement-overdue) is invalid.
orders:persistence_failed500An invariant failed while saving the order (model / data layer error).
orders:shipping_info_required400Required shipping information is missing (carrier, trackingNumber, or shipmentId).
orders:shipment_not_found404The order has no shipment with the indicated _id.
orders:shipping_label_not_available422The carrier hasn't generated the shipping label yet.
orders:invalid_state422Generic transport-layer code, emitted when the case is more specific than an orders:invalid_transition / orders:cannot_* but doesn't yet have its own dedicated code.
orders:items_required400At least one item is required for the operation; validated before reaching business logic.

returns: — returns and refunds

CodeStatusWhen it occurs
returns:not_found404No return with that ID exists.
returns:already_approved409The return was already approved.
returns:already_rejected409The return was already rejected.
returns:invalid_state422The requested state transition is not valid from the return's current state.
returns:items_required400At least one item is required to create the return.
returns:refund_failed502The upstream refund gateway (payment provider) failed.
returns:already_processed409The return was already processed.

validation: — input validation

CodeStatusWhen it occurs
validation:invalid_input400Generic schema/field validation on the request body.
validation:invalid_pagination400page or limit out of range or malformed.
validation:invalid_date_range400The startDate/endDate range is invalid (for example, startDate later than endDate).
validation:invalid_value400The value sent is not in the allowed set (invalid enum).
validation:tenant_required400The tenant is missing from the request context.
validation:missing_field400A required field is missing.
validation:invalid_format400The field has an incorrect format (date, email, ID, etc.).
validation:invalid_json400The request body is not valid JSON.
validation:field_too_long400The field exceeds the maximum allowed length.
validation:field_too_short400The field doesn't reach the minimum required length.

attachments: — order attachments

CodeStatusWhen it occurs
attachments:invalid_content_type400The contentType declared when requesting the upload URL is not valid.
attachments:invalid_attachment_type400The attachment's type is not in the allowed types catalog.
attachments:validation_failed400Generic attachment validation.
attachments:attachment_limit_exceeded400The allowed attachment limit for the order was exceeded.
attachments:unauthorized403Not authorized to operate on that attachment.
attachments:file_not_found400The referenced file doesn't exist in storage.
attachments:order_not_found404The order the file is being attached to doesn't exist.
attachments:attachment_not_found404No attachment with that ID exists.
attachments:delete_failed500Deleting the attachment failed.
attachments:upload_failed500Uploading the file failed.
attachments:invalid_type400Invalid attachment type.
attachments:file_too_large400The file exceeds the maximum allowed size.
attachments:invalid_mime_type400The file's MIME type is not allowed.

channels: / service: — infrastructure

CodeStatusWhen it occurs
channels:sync_failed502The channel's integration lambda returned an error while syncing.
service:peer_unavailable503An optional peer service (not installed in this environment) is unavailable.
service:config_missing500A required environment variable is missing on the server.

auth: — authentication and authorization

CodeStatusWhen it occurs
auth:missing_tenant401The request doesn't carry a resolved tenant.
auth:invalid_token401The API key is invalid, expired, or was revoked.
auth:permission_denied403The API key doesn't have the permission (resource:action) required for this endpoint.
auth:tenant_suspended403The tenant associated with the API key is suspended.
auth:tenant_not_found404The tenant associated with the API key doesn't exist.

abandoned_carts: — abandoned carts

CodeStatusWhen it occurs
abandoned_carts:not_found404No abandoned cart with that ID exists.
abandoned_carts:already_recovered409The cart was already marked as recovered.
abandoned_carts:already_closed409The cart was already closed.
abandoned_carts:recovery_failed500The cart recovery attempt failed.

Underscore in the error code, hyphen in the permission

This group's error namespace uses an underscore: abandoned_carts:*. The permission your API key needs for these endpoints uses a hyphen: abandoned-carts:read, abandoned-carts:recover, etc. They are not the same string — check which one you need in each context (error code vs. permission scope).

sync: — channel synchronization

CodeStatusWhen it occurs
sync:channel_not_found404The referenced channel doesn't exist.
sync:channel_disabled409The channel is disabled.
sync:integration_error502The integration failed while syncing.
sync:rate_limited429A rate limit was reached during the channel sync.

Tip

sync:integration_error and channels:sync_failed cover the same case (integration failure while syncing) but come from two different layers of the code: sync:* is an earlier wrapper and channels:sync_failed is the current code backed by the library. Handle both if your integration depends on this flow.

internal: / route: — transport layer

CodeStatusWhen it occurs
internal:server_error500Generic, unclassified server error.
internal:database_error500Database error.
internal:timeout504The operation exceeded the time limit.
internal:unknown500Unidentified error — last-resort fallback.
route:not_found404No route is registered for the requested path.
route:method_not_allowed405The HTTP method is not supported for that route.

Best practices

Log the code, not the message

Error messages can change text between versions. Codes are stable — branch your logic on code, never on the content of message.

// Correct
if (error.code === "orders:cannot_cancel") {
  await notifyMerchant(order);
}
 
// Fragile — the text can change
if (error.message.includes("cannot be cancelled")) { /* ... */ }

When to retry and when to fail fast

StatusRetry?Strategy
400 validationNoFix the request before retrying.
401 authNoRotate the API key or review authentication.
403 permissionsNoRequest the missing permission scope.
404 not foundNoThe resource doesn't exist with that ID in your tenant.
409 business conflictSometimesOnly if you can resolve the state conflict before retrying (e.g. orders:already_accepted).
422 invalid stateNoThe operation is not valid for the resource's current state; check _links before retrying.
429 sync:rate_limitedYesWait before retrying the sync.
500/502YesExponential backoff — usually transient.
503 service:peer_unavailableYesLonger backoff — the dependency is recovering.
504 internal:timeoutYesRetry with backoff; consider reducing the request size if it's a large batch.

Advertencia

A 409 is not always safely retryable: orders:duplicate means the resource already exists — retrying the creation as-is will repeat the same error. Check the specific code, not just the HTTP status.

Next steps