Devoluciones y Reembolsos

La API de devoluciones modela el ciclo completo de una RMA: se crea una devolución sobre un pedido, se aprueba o rechaza, el cliente envía la mercancía de regreso, el comerciante la recibe y finalmente se procesa el reembolso (o intercambio). Cada paso es un endpoint separado, con su propio permiso.

Progresión real de estados

El catálogo real de estados de una devolución tiene 12 valores: return-requested, return-approved, return-rejected, return-shipped, return-received, inspecting, refund-pending, refunded, exchange-pending, exchange-shipped, exchange-completed, closed. Nota que además del flujo de reembolso existe un flujo de intercambio (exchange-*) — el tipo de devolución (type) puede ser refund, exchange o store-credit.

Envelope de respuesta

Toda respuesta exitosa de recurso único sigue { "data": {...} }; las listas usan { "data": [...], "meta": { "pagination": {...} } }. Los errores siempre son { "error": { "code", "message", "details?" } }.

Listar pedidos con devoluciones

Lista paginada de pedidos que tienen al menos una devolución.

pagenumber

Número de página

limitnumber

Elementos por página

statusstring

Filtrar por estado de devolución

200
{
  "data": [
    {
      "_id": "65f3a1b2c4d5e6f7a8b9c0d1",
      "externalId": "FEN-10042",
      "returns": [{ "returnId": "65f3a1b2c4d5e6f7a8b9c0e0", "status": "return-approved" }]
    }
  ],
  "meta": {
    "pagination": { "page": 0, "limit": 20, "total": 47, "totalPages": 3, "hasMore": true }
  }
}

Permiso requerido: returns:read

curl "https://api.fenicia.io/orders/returns?status=return-approved&limit=20" \
  -H "Authorization: Bearer fkapi_tu_api_key"

Conteo de devoluciones

Devuelve el conteo de devoluciones agrupado por estado.

200
{ "data": { "...": "..." } }

Permiso requerido: returns:read

Grupos de conteo no confirmados

El endpoint existe y devuelve un conteo por grupo de estado de devolución, pero los nombres exactos de esos grupos no fueron confirmados en esta auditoría (no asumimos que coincidan con los grupos de estado de pedidos — pending, preparing, completed, cancelled, problematic, all — porque son catálogos distintos). Verifica el shape exacto contra una respuesta real antes de parsearlo por nombre de campo.

curl https://api.fenicia.io/orders/returns/counts \
  -H "Authorization: Bearer fkapi_tu_api_key"

Listar devoluciones de un pedido

Lista todas las devoluciones registradas sobre un pedido específico.

orderIdstringrequired

ID del pedido

200
{
  "data": [
    { "returnId": "65f3a1b2c4d5e6f7a8b9c0e0", "status": "return-approved", "type": "refund" }
  ]
}

Permiso requerido: returns:read

Crear una devolución

Crea una devolución sobre un pedido.

orderIdstringrequired

ID del pedido

itemsarrayrequired

Ítems del pedido a devolver, identificados por sku (no por índice). Mínimo 1 elemento: { sku, quantity, reason? }.

reasonstringrequired

Motivo de la devolución (ver catálogo abajo).

typestring

Tipo de devolución: refund, exchange o store-credit. No enforced en runtime (ver nota abajo).

requestedBystring

Quién solicita la devolución: customer, seller, platform o marketplace. Default: seller.

initialStatusstring

Estado inicial de la devolución (para devoluciones manuales creadas ya en un estado avanzado, ej. return-received).

overallConditionstring

Condición general de la mercancía, si ya se conoce al crear (ver catálogo en la sección de recepción).

shipmentInfoobject

Información de envío de retorno, si ya se conoce al crear (devoluciones manuales).

{
  "items": [{ "sku": "CAMISA-AZUL-M", "quantity": 1 }],
  "reason": "defective"
}
201
{
  "data": {
    "returnId": "65f3a1b2c4d5e6f7a8b9c0e0",
    "status": "return-requested",
    "type": "refund",
    "requestedBy": "seller",
    "reason": "defective",
    "items": [{ "sku": "CAMISA-AZUL-M", "title": "Camisa azul", "quantity": 1, "reason": "defective", "condition": "pending-inspection" }],
    "requestedAt": "2026-07-20T18:30:00.000Z"
  }
}

Permiso requerido: returns:create

Catálogo real de reason (10 valores)

defective, wrong-item, size-fit, quality-issue, not-as-described, changed-mind, arrived-late, damaged-shipping, duplicate-order, other

Formato con guion, no guion bajo

Todos los valores de reason usan guion (wrong-item, no wrong_item). Enviar un valor con guion bajo falla la validación.

items se identifica por sku, no por itemIndex

A diferencia de POST /orders/{orderId}/prepare (que identifica ítems por itemIndex), la creación de una devolución identifica cada ítem por sku: el servicio busca ese SKU dentro de order.items y falla con OrderValidationError si no lo encuentra, o si quantity excede la cantidad original del pedido.

type no se exige en runtime pese a que el tipo TypeScript lo marca requerido

El validador de transporte (createReturnSchema) solo exige items y reason — no valida type en absoluto. Y a nivel de librería, createReturn() asigna newReturn.type = input.type directamente, sin ningún chequeo: si omites type, el campo queda undefined en el documento guardado, sin error. La interfaz TypeScript CreateReturnInput marca type como no-opcional, pero eso NO se traduce en una validación real en ninguna capa — es una promesa de tipo que el código no cumple. Si tu integración depende de que toda devolución tenga type, envíalo explícitamente; no asumas un default.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/returns \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"itemIndex":0,"quantity":1}],"reason":"defective"}'

Consultar una devolución

Obtiene el detalle de una devolución específica.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

200
{
  "data": {
    "returnId": "65f3a1b2c4d5e6f7a8b9c0e0",
    "orderId": "65f3a1b2c4d5e6f7a8b9c0d1",
    "status": "return-approved",
    "reason": "defective",
    "type": "refund"
  }
}
404
{ "error": { "code": "returns:not_found", "message": "Return not found" } }

Permiso requerido: returns:read

Actualizar una devolución

Actualiza campos de una devolución existente. Se requiere al menos un campo en el body.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

statusstring

Nuevo estado de la devolución.

overallConditionstring

Condición general de la mercancía (ver catálogo en la sección de recepción).

inspectionNotesstring

Notas de la inspección de la mercancía.

rejectionReasonstring

Motivo de rechazo, si aplica.

skipMerchandiseReturnboolean

Si se omite el retorno físico de la mercancía (ej. reembolso sin devolución).

inventoryDispositionobject

Qué hacer con el inventario devuelto: { locationId, locationName?, placeType: 'available'|'reconditioning'|'damaged'|'shrinkage', placeCode?, costImpact?, currency? }.

refundobject

Datos de reembolso asociados: { amount, currency, method, status, transactionId? }.

shipmentInfoobject

Información del envío de retorno: { trackingNumber?, carrier?, customCarrierName?, trackingUrl?, waybillId?, labelUrl?, shippedAt?, receivedAt?, cost? }.

{
  "inspectionNotes": "Producto sin daños visibles",
  "overallCondition": "like-new"
}
200
{
  "data": {
    "returnId": "65f3a1b2c4d5e6f7a8b9c0e0",
    "status": "inspecting",
    "inspectionNotes": "Producto sin daños visibles"
  }
}

Permiso requerido: returns:update

curl -X PUT https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/returns/65f3a1b2c4d5e6f7a8b9c0e0 \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"inspectionNotes":"Producto sin daños visibles"}'

Aprobar una devolución

Aprueba una devolución pendiente. No requiere body.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

{}
200
{ "data": { "returnId": "65f3a1b2c4d5e6f7a8b9c0e0", "status": "return-approved" } }

Permiso requerido: returns:approve

No requiere cuerpo

El handler no lee ningún campo del body — envía {} o simplemente omite el cuerpo.

Rechazar una devolución

Rechaza una devolución pendiente, con un motivo obligatorio.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

reasonstringrequired

Motivo del rechazo (texto libre, no un enum del dominio).

{
  "reason": "Fuera de la ventana de devoluciones"
}
200
{ "data": { "returnId": "65f3a1b2c4d5e6f7a8b9c0e0", "status": "return-rejected" } }

Permiso requerido: returns:approve

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/returns/65f3a1b2c4d5e6f7a8b9c0e0/reject \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Fuera de la ventana de devoluciones"}'

Registrar el envío de retorno

Registra que el cliente envió de regreso la mercancía de la devolución.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

trackingNumberstringrequired

Número de rastreo del envío de retorno.

carrierstringrequired

Código de la paquetería.

waybillIdstring

ID de la guía/waybill.

labelUrlstring

URL de la guía en PDF.

costobject

Costo del envío de retorno: { labelCost, feniciaFee?, paidBy: 'customer'|'seller', currency }.

originobject

Dirección de origen del envío de retorno.

destinationobject

Dirección de destino del envío de retorno.

{
  "trackingNumber": "1Z999AA10123456784",
  "carrier": "fedex"
}
200
{
  "data": {
    "returnId": "65f3a1b2c4d5e6f7a8b9c0e0",
    "status": "return-shipped",
    "shippedAt": "2026-07-20T18:30:00.000Z",
    "shipmentInfo": { "trackingNumber": "1Z999AA10123456784", "carrier": "fedex", "shippedAt": "2026-07-20T18:30:00.000Z" }
  }
}

Permiso requerido: returns:update

Recibir la mercancía devuelta

Registra la recepción de la mercancía devuelta, con su condición.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

conditionstringrequired

Condición de la mercancía recibida (ver catálogo abajo).

notesstring

Notas de la recepción.

{
  "condition": "like-new"
}
200
{ "data": { "returnId": "65f3a1b2c4d5e6f7a8b9c0e0", "status": "inspecting", "overallCondition": "like-new" } }

Permiso requerido: returns:process

Catálogo real de condition (7 valores)

unopened, like-new, used-acceptable, used-poor, damaged, defective, pending-inspection

Formato con guion, no guion bajo

Ningún valor de condition coincide con new/used/damaged/unusable — esos no existen en el catálogo real. Usa exactamente uno de los 7 valores con guion listados arriba.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/returns/65f3a1b2c4d5e6f7a8b9c0e0/receive \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"condition":"like-new"}'

Procesar el reembolso

Procesa el reembolso de una devolución ya recibida/inspeccionada.

orderIdstringrequired

ID del pedido

returnIdstringrequired

ID de la devolución

amountnumberrequired

Monto a reembolsar (> 0).

currencystringrequired

Moneda de 3 letras (ej. MXN).

methodstringrequired

Método de reembolso (ver catálogo abajo).

transactionIdstring

ID de la transacción del método de pago, si aplica.

{
  "amount": 799.50,
  "currency": "MXN",
  "method": "original-payment"
}
200
{
  "data": {
    "returnId": "65f3a1b2c4d5e6f7a8b9c0e0",
    "status": "refunded",
    "refund": { "amount": 799.5, "currency": "MXN", "method": "original-payment" }
  }
}
502
{ "error": { "code": "returns:refund_failed", "message": "Refund could not be processed" } }

Permiso requerido: returns:process

Catálogo real de method (exactamente 3 valores)

original-payment, store-credit, bank-transfer

Formato con guion, exactamente 3 valores

El catálogo real tiene exactamente estos 3 valores, con guion. No existe un cuarto valor genérico ("etc."); enviar original_payment (guion bajo) falla la validación.

curl -X POST https://api.fenicia.io/orders/65f3a1b2c4d5e6f7a8b9c0d1/returns/65f3a1b2c4d5e6f7a8b9c0e0/refund \
  -H "Authorization: Bearer fkapi_tu_api_key" \
  -H "Content-Type: application/json" \
  -d '{"amount":799.50,"currency":"MXN","method":"original-payment"}'

Errores

CódigoStatusDescripción
returns:not_found404No existe una devolución con ese ID sobre ese pedido.
returns:already_approved409La devolución ya fue aprobada.
returns:already_rejected409La devolución ya fue rechazada.
returns:invalid_state422La operación no es válida en el estado actual de la devolución.
returns:items_required400El body no incluye ítems (o el arreglo está vacío).
returns:refund_failed502El reembolso no se pudo procesar contra el método de pago.
returns:already_processed409La devolución ya fue procesada previamente.
orders:not_found404No existe un pedido con ese ID en tu tenant.
auth:permission_denied403La API key no tiene el permiso requerido.
auth:invalid_token401La API key es inválida o fue revocada.

Siguientes pasos