Transiciones de estado

Fenicia modela el ciclo de vida de un pedido como una máquina de estados finita: desde un estado dado, solo un conjunto específico de estados destino son válidos. Este artículo documenta la máquina completa (23 estados) y todos los endpoints que la operan.

¿Qué endpoint debo usar?

Usa accept, reject y cancel para las acciones comunes del comerciante — están optimizadas para su caso de uso y validan reglas de negocio específicas (por ejemplo, el catálogo de razones de cancelación). Usa el endpoint genérico transition cuando necesites mover el pedido a cualquier estado válido de la máquina que no tenga una acción dedicada.

La máquina de estados completa

23 estados. Cada fila indica los estados a los que puedes transicionar desde el estado actual; cualquier otro destino es rechazado con orders:invalid_transition.

Estado actualTransiciones válidas hacia
draftnew, cancelled
new (alias de pending)accepted, cancelled, rejected
pending (alias de new)accepted, cancelled, rejected
acceptedprocessing, preparing, cancelled
processingpackaging, cancelled, fulfilled
preparingfulfilled, cancelled
packagingpickup-pending, fulfilled, cancelled
pickup-pendingin-transit, fulfilled
ready_for_pickupfulfilled
fulfilledin-transit, delivered, partially-delivered
shippedin-transit, delivered
in-transitdelivered, exception, not-delivered, partially-delivered
out_for_deliverydelivered, exception, not-delivered
deliveredreturned
completedreturned
partially-delivereddelivered, returned
partially-fulfilledfulfilled, in-transit, delivered
exceptionin-transit, delivered, not-delivered, cancelled
not-deliveredin-transit, cancelled, returned
problematiccancelled
cancelled(terminal — sin transiciones salientes)
rejected(terminal — sin transiciones salientes)
returned(terminal — sin transiciones salientes)

Estados terminales

cancelled, rejected y returned son estados terminales: un pedido que llega a cualquiera de ellos no puede transicionar a ningún otro estado.

Tip

new y pending son alias del mismo estado — tienen exactamente las mismas transiciones válidas. Puedes usar cualquiera de los dos indistintamente.

Acciones disponibles (HATEOAS)

Cuando consultas un pedido, su objeto _links incluye únicamente las acciones válidas para su estado actual. Las acciones posibles son: accept, reject, cancel, prepare, fulfill, returns. Si una acción no aparece en _links, no es válida en el estado actual del pedido.

Aceptar un pedido

Acepta un pedido pendiente. Requiere permiso orders:update.

orderIdstringrequired

ID del pedido.

{}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "accepted",
    "_links": { "self": "/orders/65f3a1b2c4d5e6f7a8b9c0d1" }
  }
}

No requiere cuerpo

El handler no lee ningún campo del cuerpo de la petición — envía {} o simplemente omite el body. Si el pedido pertenece a un canal que exige confirmar la aceptación en el marketplace antes de aceptar localmente (hoy: Walmart), la aceptación se propaga automáticamente; no necesitas enviar nada para eso.

Permiso requerido: orders:update

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/accept \
  -H "Authorization: Bearer fkapi_tu_api_key"

Rechazar un pedido

Rechaza un pedido pendiente. Requiere permiso orders:cancel.

orderIdstringrequired

ID del pedido.

reasonstringrequired

Una de las 12 razones canónicas de cancelación — mismo catálogo que usa cancel (ver tabla abajo).

motivestring

Motivo hijo específico dentro de la razón elegida. Debe pertenecer a reason.

notestring

Contexto adicional en texto libre. Obligatorio cuando reason es other.

requestedBystring

Quién solicitó el rechazo: buyer, seller o platform.

{
  "reason": "out_of_stock"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "rejected",
    "_links": {}
  }
}

Permiso requerido: orders:cancel

Mismo catálogo de reason que cancel, validación estricta

reject usa el mismo catálogo de 12 razones canónicas que cancel (ver la tabla en la sección "Cancelar un pedido" abajo) y la misma regla: note es obligatorio cuando reason es other. A diferencia de cancel (que acepta campos extra sin rechazarlos), el validador de reject es estricto: enviar un campo que no sea reason, motive, note o requestedBy es rechazado.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/reject \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"reason": "out_of_stock"}'

Cancelar un pedido

Cancela un pedido. Requiere permiso orders:cancel.

orderIdstringrequired

ID del pedido.

reasonstringrequired

Una de las 12 razones canónicas de cancelación (ver tabla abajo).

motivestring

Motivo hijo específico dentro de la razón elegida.

notestring

Contexto adicional en texto libre. Obligatorio cuando reason es other.

requestedBystring

Quién solicitó la cancelación: buyer, seller o platform.

{
  "reason": "customer_request",
  "requestedBy": "buyer",
  "note": "El cliente cambió de opinión"
}
200
{
  "data": {
    "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
    "orderStatus": "cancelled",
    "_links": {}
  }
}

Permiso requerido: orders:cancel

Razones de cancelación canónicas

reason
out_of_stock
fulfillment_unavailable
customer_request
shipping_issue
payment_declined
fraud_suspected
product_issue
pricing_error
duplicate_order
merchant_decision
system_error
other

note es obligatorio cuando reason es other

Si envías reason: "other", el campo note pasa a ser obligatorio — describe el motivo real de la cancelación.

requestedBy acepta exactamente uno de estos tres valores: buyer, seller, platform.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/cancel \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "customer_request",
    "requestedBy": "buyer",
    "note": "El cliente cambió de opinión"
  }'

Cambiar estado directamente

Establece el estado del pedido sin pasar por una acción con nombre. Útil cuando integras un flujo que ya conoce el estado destino exacto.

Establece directamente el estado de un pedido. Requiere permiso orders:update.

orderIdstringrequired

ID del pedido.

statusstringrequired

Estado objetivo.

{
  "status": "preparing"
}
204Sin cuerpo
(sin contenido)

Permiso requerido: orders:update

204 No Content — sin cuerpo

A diferencia de accept/reject/cancel/transition, este endpoint responde 204 No Content: no valida la transición contra la máquina de estados (no confundir con transition, que sí valida) y no devuelve el pedido actualizado. Si necesitas el pedido resultante, haz un GET /orders/{orderId} después.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/status \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"status": "preparing"}'

Transición genérica

Transiciona un pedido al estado indicado, validando la máquina de estados. Requiere permiso orders:update.

orderIdstringrequired

ID del pedido.

toStatusstringrequired

Estado objetivo. Debe ser una transición válida desde el estado actual.

reasonstring

Motivo de la transición, para el log de actividad. Obligatorio solo cuando la transición específica lo exige (ej. algunas transiciones a cancelled/rejected).

detailsstring

Detalle adicional en texto libre para el log de actividad.

flowProfilestring

Perfil de flujo contra el que se valida la transición: basic, intermediate (default) o wms-full.

{
  "toStatus": "preparing"
}
200
{
  "data": {
    "order": { "_id": "65f3a1b2c4d5e6f7a8b9c0d1", "orderStatus": "preparing" },
    "previousStatus": "accepted",
    "newStatus": "preparing",
    "hooks": []
  }
}

Permiso requerido: orders:update

reason no es requerido en general

El schema de transporte de este endpoint solo exige toStatus. reason no es obligatorio salvo que la transición puntual lo requiera (por ejemplo, transicionar a cancelled o rejected desde ciertos estados) — en ese caso, omitirlo devuelve validation:invalid_input.

La respuesta NO trae _links, y el pedido va anidado en order

A diferencia de accept/reject/cancel (que devuelven { data: { ...order, _links } }), este endpoint devuelve { data: { order, previousStatus, newStatus, hooks } } — el pedido completo vive en la clave order, sin _links, junto con el estado anterior/nuevo y la lista de hooks disparados (ej. ajustes de inventario).

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/transition \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"toStatus": "preparing"}'

Transición masiva

Aplica la misma transición a varios pedidos en una sola llamada.

Transiciona varios pedidos a la vez. Requiere permiso orders:update.

orderIdsarrayrequired

IDs de los pedidos a transicionar.

toStatusstringrequired

Estado objetivo para todos los pedidos.

reasonstring

Razón aplicada al lote. No es requerida por el schema.

detailsobject

Metadatos adicionales del lote.

flowProfilestring

Perfil de flujo opcional para la transición.

{
  "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1", "65f3a1b2c4d5e6f7a8b9c0d2"],
  "toStatus": "accepted"
}
200
{
  "data": {
    "success": true,
    "bulkOperationId": "bulk_1721500000000_abc123",
    "summary": { "total": 2, "successful": 2, "failed": 0, "skipped": 0 },
    "results": [
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d1", "success": true, "previousStatus": "pending", "newStatus": "accepted" },
      { "orderId": "65f3a1b2c4d5e6f7a8b9c0d2", "success": true, "previousStatus": "pending", "newStatus": "accepted" }
    ],
    "targetStatus": "accepted",
    "performedAt": "2026-07-20T18:30:00.000Z",
    "performedBy": { "userId": "65f0a0b0c0d0e0f0a0b0c0d0" }
  }
}

Permiso requerido: orders:update

orderIds vacío falla la operación completa

orderIds debe tener al menos un elemento — un arreglo vacío rechaza toda la llamada con validation:invalid_input, no hay resultado parcial en ese caso. Con al menos un elemento, cada pedido se procesa individualmente: uno puede fallar (transición inválida, no encontrado) sin abortar los demás — revisa results[] para el detalle por pedido.

curl -X POST https://api.fenicia.io/orders/bulk/transition \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "orderIds": ["65f3a1b2c4d5e6f7a8b9c0d1", "65f3a1b2c4d5e6f7a8b9c0d2"],
    "toStatus": "accepted"
  }'

Consultar las transiciones válidas de un pedido

Devuelve los estados a los que un pedido puede transicionar desde su estado actual. Requiere permiso orders:read.

orderIdstringrequired

ID del pedido.

flowProfilestring

Perfil de flujo contra el que se calculan las transiciones válidas: basic, intermediate (default) o wms-full.

200
{
  "data": {
    "currentStatus": "accepted",
    "validTargets": ["processing", "preparing", "cancelled"]
  }
}

Permiso requerido: orders:read

Este endpoint te evita tener que reimplementar la tabla de la máquina de estados en tu cliente: te devuelve directamente el conjunto de estados válidos para el pedido consultado. Está documentado en detalle en Consultar un pedido, junto con el resto de los enlaces HATEOAS del recurso.

Errores

CódigoStatusDescripción
orders:invalid_transition422El toStatus/status solicitado no es una transición válida desde el estado actual del pedido.
orders:not_found404No existe un pedido con ese ID en tu tenant.
orders:already_accepted409El pedido ya fue aceptado previamente.
orders:already_cancelled409El pedido ya está cancelado.
orders:already_fulfilled409El pedido ya fue surtido.
orders:cannot_cancel422El pedido está en un estado que no permite cancelación.
validation:invalid_input400El cuerpo no cumple el schema (por ejemplo, reason fuera del catálogo de 12 valores).
auth:invalid_token401La API key es inválida o fue revocada.
auth:permission_denied403La API key no tiene el scope requerido.

Consulta el catálogo completo de errores para el resto de los códigos del dominio.

Siguientes pasos