Catálogo de Errores
Esta página es la referencia canónica de todos los códigos de error que puede devolver la API de Órdenes. Los códigos están agrupados por namespace (orders:, returns:, validation:, attachments:, channels:/service:, auth:, abandoned_carts:, sync:, internal:, route:), que corresponde al dominio que originó el error, no al endpoint que lo devolvió — el mismo código puede aparecer en varios endpoints distintos.
Forma de la respuesta de error
Toda respuesta de error de este dominio tiene exactamente esta forma:
{
"error": {
"code": "orders:not_found",
"message": "Order not found",
"details": {
"field": "orderId",
"value": "65f3a1b2c4d5e6f7a8b9c0d1"
}
}
}| Campo | Tipo | Descripción |
|---|---|---|
error.code | string | Identificador estable, formato namespace:snake_case. Ramifica tu lógica por este campo. |
error.message | string | Descripción legible pensada para logs/debug. Puede cambiar de texto entre versiones — nunca la parsees. |
error.details | objeto (opcional) | Contexto adicional. Puede traer field, value, allowedValues, u otras claves según el error. |
El código nunca es SCREAMING_SNAKE_CASE
Todo code sigue el formato namespace:snake_case en minúsculas (orders:not_found, returns:already_approved). Si ves un código en mayúsculas sin namespace en algún ejemplo desactualizado, no es un código real de esta API.
Cómo manejar un 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":
// el pedido no existe en tu tenant
break;
case "auth:permission_denied":
// tu API key no tiene el scope requerido
break;
default:
// maneja el resto por status HTTP + code
console.error(body.error.code, body.error.message);
}
}orders: — ciclo de vida y persistencia del pedido
| Código | Status | Cuándo ocurre |
|---|---|---|
orders:not_found | 404 | No existe un pedido con ese ID en tu tenant. |
orders:duplicate | 409 | Ya existe un pedido con la misma combinación tenant + externalId + canal. |
orders:invalid_transition | 422 | La transición de estado solicitada no es válida desde el estado actual. Ver Transiciones de estado. |
orders:already_accepted | 409 | El pedido ya fue aceptado previamente. |
orders:already_fulfilled | 409 | El pedido ya fue marcado como surtido. |
orders:already_cancelled | 409 | El pedido ya fue cancelado. |
orders:cannot_cancel | 422 | El estado actual del pedido no permite cancelarlo. |
orders:cannot_fulfill | 422 | El estado actual del pedido no permite surtirlo. |
orders:cannot_confirm_delivery | 422 | El pedido está en un estado no entregable; no se puede confirmar su entrega. |
orders:channel_unsupported | 422 | El canal del pedido no soporta la operación solicitada. |
orders:settlement_query_invalid | 400 | La ventana de fechas de una consulta de liquidación (settlement-overdue) es inválida. |
orders:persistence_failed | 500 | Falló una invariante al guardar el pedido (error de modelo / capa de datos). |
orders:shipping_info_required | 400 | Falta información de envío requerida (carrier, trackingNumber o shipmentId). |
orders:shipment_not_found | 404 | El pedido no tiene ningún envío con el _id indicado. |
orders:shipping_label_not_available | 422 | La paquetería aún no ha generado la guía de envío. |
orders:invalid_state | 422 | Código genérico de la capa de transporte, emitido cuando el caso es más específico que un orders:invalid_transition / orders:cannot_* pero aún no tiene su propio código dedicado. |
orders:items_required | 400 | Se requiere al menos un item en la operación; validado antes de llegar a la lógica de negocio. |
returns: — devoluciones y reembolsos
| Código | Status | Cuándo ocurre |
|---|---|---|
returns:not_found | 404 | No existe una devolución con ese ID. |
returns:already_approved | 409 | La devolución ya fue aprobada. |
returns:already_rejected | 409 | La devolución ya fue rechazada. |
returns:invalid_state | 422 | La transición de estado solicitada no es válida desde el estado actual de la devolución. |
returns:items_required | 400 | Se requiere al menos un item para crear la devolución. |
returns:refund_failed | 502 | Falló el gateway de reembolso upstream (proveedor de pago). |
returns:already_processed | 409 | La devolución ya fue procesada. |
validation: — validación de entrada
| Código | Status | Cuándo ocurre |
|---|---|---|
validation:invalid_input | 400 | Validación genérica de schema/campo sobre el body de la petición. |
validation:invalid_pagination | 400 | page o limit fuera de rango o mal formados. |
validation:invalid_date_range | 400 | El rango startDate/endDate es inválido (por ejemplo, startDate posterior a endDate). |
validation:invalid_value | 400 | El valor enviado no está en el conjunto permitido (enum inválido). |
validation:tenant_required | 400 | Falta el tenant en el contexto de la petición. |
validation:missing_field | 400 | Falta un campo obligatorio. |
validation:invalid_format | 400 | El campo tiene un formato incorrecto (fecha, email, ID, etc.). |
validation:invalid_json | 400 | El body de la petición no es JSON válido. |
validation:field_too_long | 400 | El campo excede la longitud máxima permitida. |
validation:field_too_short | 400 | El campo no alcanza la longitud mínima requerida. |
attachments: — adjuntos de pedidos
| Código | Status | Cuándo ocurre |
|---|---|---|
attachments:invalid_content_type | 400 | El contentType declarado al pedir la URL de subida no es válido. |
attachments:invalid_attachment_type | 400 | El type del adjunto no está en el catálogo de tipos permitidos. |
attachments:validation_failed | 400 | Validación genérica del adjunto. |
attachments:attachment_limit_exceeded | 400 | Se superó el límite de adjuntos permitidos para el pedido. |
attachments:unauthorized | 403 | Sin autorización para operar sobre ese adjunto. |
attachments:file_not_found | 400 | El archivo referenciado no existe en el almacenamiento. |
attachments:order_not_found | 404 | El pedido al que se intenta adjuntar el archivo no existe. |
attachments:attachment_not_found | 404 | No existe un adjunto con ese ID. |
attachments:delete_failed | 500 | Falló la eliminación del adjunto. |
attachments:upload_failed | 500 | Falló la subida del archivo. |
attachments:invalid_type | 400 | Tipo de adjunto inválido. |
attachments:file_too_large | 400 | El archivo excede el tamaño máximo permitido. |
attachments:invalid_mime_type | 400 | El MIME type del archivo no está permitido. |
channels: / service: — infraestructura
| Código | Status | Cuándo ocurre |
|---|---|---|
channels:sync_failed | 502 | El lambda de integración del canal devolvió un error al sincronizar. |
service:peer_unavailable | 503 | Un servicio par opcional (no instalado en este entorno) no está disponible. |
service:config_missing | 500 | Falta una variable de entorno requerida en el servidor. |
auth: — autenticación y autorización
| Código | Status | Cuándo ocurre |
|---|---|---|
auth:missing_tenant | 401 | La petición no trae un tenant resuelto. |
auth:invalid_token | 401 | El API key es inválido, expiró o fue revocado. |
auth:permission_denied | 403 | El API key no tiene el permiso (resource:action) requerido para este endpoint. |
auth:tenant_suspended | 403 | El tenant asociado al API key está suspendido. |
auth:tenant_not_found | 404 | El tenant asociado al API key no existe. |
abandoned_carts: — carritos abandonados
| Código | Status | Cuándo ocurre |
|---|---|---|
abandoned_carts:not_found | 404 | No existe un carrito abandonado con ese ID. |
abandoned_carts:already_recovered | 409 | El carrito ya fue marcado como recuperado. |
abandoned_carts:already_closed | 409 | El carrito ya fue cerrado. |
abandoned_carts:recovery_failed | 500 | Falló el intento de recuperación del carrito. |
Guion bajo en el código de error, guion en el permiso
El namespace de error de este grupo usa guion bajo: abandoned_carts:*. El permiso que necesita tu API key para estos endpoints usa guion: abandoned-carts:read, abandoned-carts:recover, etc. No son el mismo string — revisa cuál necesitas en cada contexto (código de error vs. scope de permiso).
sync: — sincronización con canales
| Código | Status | Cuándo ocurre |
|---|---|---|
sync:channel_not_found | 404 | El canal referenciado no existe. |
sync:channel_disabled | 409 | El canal está deshabilitado. |
sync:integration_error | 502 | Falló la integración al sincronizar. |
sync:rate_limited | 429 | Se alcanzó un límite de tasa durante la sincronización con el canal. |
Tip
sync:integration_error y channels:sync_failed cubren el mismo caso (falla de la integración al sincronizar) pero vienen de dos capas distintas del código: sync:* es un envoltorio anterior y channels:sync_failed es el código actual respaldado por la librería. Maneja ambos si tu integración depende de este flujo.
internal: / route: — capa de transporte
| Código | Status | Cuándo ocurre |
|---|---|---|
internal:server_error | 500 | Error genérico no clasificado del servidor. |
internal:database_error | 500 | Error de base de datos. |
internal:timeout | 504 | La operación excedió el tiempo límite. |
internal:unknown | 500 | Error no identificado — fallback de último recurso. |
route:not_found | 404 | No existe ninguna ruta registrada para el path solicitado. |
route:method_not_allowed | 405 | El método HTTP no está soportado para esa ruta. |
Buenas prácticas
Loguea el code, no el message
Los mensajes de error pueden cambiar de texto entre versiones. Los códigos son estables — ramifica tu lógica por code, nunca por el contenido de message.
// Correcto
if (error.code === "orders:cannot_cancel") {
await notifyMerchant(order);
}
// Frágil — el texto puede cambiar
if (error.message.includes("cannot be cancelled")) { /* ... */ }Cuándo reintentar y cuándo fallar rápido
| Status | ¿Reintentar? | Estrategia |
|---|---|---|
400 validación | No | Corrige la petición antes de reintentar. |
401 auth | No | Rota el API key o revisa la autenticación. |
403 permisos | No | Solicita el scope de permiso que falta. |
404 no encontrado | No | El recurso no existe con ese ID en tu tenant. |
409 conflicto de negocio | A veces | Solo si puedes resolver el conflicto de estado antes de reintentar (p. ej. orders:already_accepted). |
422 estado inválido | No | La operación no es válida para el estado actual del recurso; consulta _links antes de reintentar. |
429 sync:rate_limited | Sí | Espera antes de reintentar la sincronización. |
500/502 | Sí | Backoff exponencial — suele ser transitorio. |
503 service:peer_unavailable | Sí | Backoff más largo — la dependencia se está recuperando. |
504 internal:timeout | Sí | Reintenta con backoff; considera reducir el tamaño de la petición si es un lote grande. |
Advertencia
El 409 no siempre es reintentable de forma segura: orders:duplicate significa que el recurso ya existe — reintentar la creación tal cual repetirá el mismo error. Revisa el code específico, no solo el status HTTP.