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"
    }
  }
}
CampoTipoDescripción
error.codestringIdentificador estable, formato namespace:snake_case. Ramifica tu lógica por este campo.
error.messagestringDescripción legible pensada para logs/debug. Puede cambiar de texto entre versiones — nunca la parsees.
error.detailsobjeto (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ódigoStatusCuándo ocurre
orders:not_found404No existe un pedido con ese ID en tu tenant.
orders:duplicate409Ya existe un pedido con la misma combinación tenant + externalId + canal.
orders:invalid_transition422La transición de estado solicitada no es válida desde el estado actual. Ver Transiciones de estado.
orders:already_accepted409El pedido ya fue aceptado previamente.
orders:already_fulfilled409El pedido ya fue marcado como surtido.
orders:already_cancelled409El pedido ya fue cancelado.
orders:cannot_cancel422El estado actual del pedido no permite cancelarlo.
orders:cannot_fulfill422El estado actual del pedido no permite surtirlo.
orders:cannot_confirm_delivery422El pedido está en un estado no entregable; no se puede confirmar su entrega.
orders:channel_unsupported422El canal del pedido no soporta la operación solicitada.
orders:settlement_query_invalid400La ventana de fechas de una consulta de liquidación (settlement-overdue) es inválida.
orders:persistence_failed500Falló una invariante al guardar el pedido (error de modelo / capa de datos).
orders:shipping_info_required400Falta información de envío requerida (carrier, trackingNumber o shipmentId).
orders:shipment_not_found404El pedido no tiene ningún envío con el _id indicado.
orders:shipping_label_not_available422La paquetería aún no ha generado la guía de envío.
orders:invalid_state422Có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_required400Se requiere al menos un item en la operación; validado antes de llegar a la lógica de negocio.

returns: — devoluciones y reembolsos

CódigoStatusCuándo ocurre
returns:not_found404No existe una devolución con ese ID.
returns:already_approved409La devolución ya fue aprobada.
returns:already_rejected409La devolución ya fue rechazada.
returns:invalid_state422La transición de estado solicitada no es válida desde el estado actual de la devolución.
returns:items_required400Se requiere al menos un item para crear la devolución.
returns:refund_failed502Falló el gateway de reembolso upstream (proveedor de pago).
returns:already_processed409La devolución ya fue procesada.

validation: — validación de entrada

CódigoStatusCuándo ocurre
validation:invalid_input400Validación genérica de schema/campo sobre el body de la petición.
validation:invalid_pagination400page o limit fuera de rango o mal formados.
validation:invalid_date_range400El rango startDate/endDate es inválido (por ejemplo, startDate posterior a endDate).
validation:invalid_value400El valor enviado no está en el conjunto permitido (enum inválido).
validation:tenant_required400Falta el tenant en el contexto de la petición.
validation:missing_field400Falta un campo obligatorio.
validation:invalid_format400El campo tiene un formato incorrecto (fecha, email, ID, etc.).
validation:invalid_json400El body de la petición no es JSON válido.
validation:field_too_long400El campo excede la longitud máxima permitida.
validation:field_too_short400El campo no alcanza la longitud mínima requerida.

attachments: — adjuntos de pedidos

CódigoStatusCuándo ocurre
attachments:invalid_content_type400El contentType declarado al pedir la URL de subida no es válido.
attachments:invalid_attachment_type400El type del adjunto no está en el catálogo de tipos permitidos.
attachments:validation_failed400Validación genérica del adjunto.
attachments:attachment_limit_exceeded400Se superó el límite de adjuntos permitidos para el pedido.
attachments:unauthorized403Sin autorización para operar sobre ese adjunto.
attachments:file_not_found400El archivo referenciado no existe en el almacenamiento.
attachments:order_not_found404El pedido al que se intenta adjuntar el archivo no existe.
attachments:attachment_not_found404No existe un adjunto con ese ID.
attachments:delete_failed500Falló la eliminación del adjunto.
attachments:upload_failed500Falló la subida del archivo.
attachments:invalid_type400Tipo de adjunto inválido.
attachments:file_too_large400El archivo excede el tamaño máximo permitido.
attachments:invalid_mime_type400El MIME type del archivo no está permitido.

channels: / service: — infraestructura

CódigoStatusCuándo ocurre
channels:sync_failed502El lambda de integración del canal devolvió un error al sincronizar.
service:peer_unavailable503Un servicio par opcional (no instalado en este entorno) no está disponible.
service:config_missing500Falta una variable de entorno requerida en el servidor.

auth: — autenticación y autorización

CódigoStatusCuándo ocurre
auth:missing_tenant401La petición no trae un tenant resuelto.
auth:invalid_token401El API key es inválido, expiró o fue revocado.
auth:permission_denied403El API key no tiene el permiso (resource:action) requerido para este endpoint.
auth:tenant_suspended403El tenant asociado al API key está suspendido.
auth:tenant_not_found404El tenant asociado al API key no existe.

abandoned_carts: — carritos abandonados

CódigoStatusCuándo ocurre
abandoned_carts:not_found404No existe un carrito abandonado con ese ID.
abandoned_carts:already_recovered409El carrito ya fue marcado como recuperado.
abandoned_carts:already_closed409El carrito ya fue cerrado.
abandoned_carts:recovery_failed500Falló 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ódigoStatusCuándo ocurre
sync:channel_not_found404El canal referenciado no existe.
sync:channel_disabled409El canal está deshabilitado.
sync:integration_error502Falló la integración al sincronizar.
sync:rate_limited429Se 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ódigoStatusCuándo ocurre
internal:server_error500Error genérico no clasificado del servidor.
internal:database_error500Error de base de datos.
internal:timeout504La operación excedió el tiempo límite.
internal:unknown500Error no identificado — fallback de último recurso.
route:not_found404No existe ninguna ruta registrada para el path solicitado.
route:method_not_allowed405El 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ónNoCorrige la petición antes de reintentar.
401 authNoRota el API key o revisa la autenticación.
403 permisosNoSolicita el scope de permiso que falta.
404 no encontradoNoEl recurso no existe con ese ID en tu tenant.
409 conflicto de negocioA vecesSolo si puedes resolver el conflicto de estado antes de reintentar (p. ej. orders:already_accepted).
422 estado inválidoNoLa operación no es válida para el estado actual del recurso; consulta _links antes de reintentar.
429 sync:rate_limitedEspera antes de reintentar la sincronización.
500/502Backoff exponencial — suele ser transitorio.
503 service:peer_unavailableBackoff más largo — la dependencia se está recuperando.
504 internal:timeoutReintenta 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.

Siguientes pasos