Returns and Refunds

The Returns API models the complete RMA lifecycle: a return is created on an order, it's approved or rejected, the customer ships the merchandise back, the merchant receives it, and finally the refund (or exchange) is processed. Each step is a separate endpoint, with its own permission.

Real state progression

The real catalog of return states has 12 values: return-requested, return-approved, return-rejected, return-shipped, return-received, inspecting, refund-pending, refunded, exchange-pending, exchange-shipped, exchange-completed, closed. Note that in addition to the refund flow there is an exchange flow (exchange-*) — the return type (type) can be refund, exchange, or store-credit.

Response envelope

Every successful single-resource response follows { "data": {...} }; lists use { "data": [...], "meta": { "pagination": {...} } }. Errors are always { "error": { "code", "message", "details?" } }.

List orders with returns

Paginated list of orders that have at least one return.

pagenumber

Page number

limitnumber

Items per page

statusstring

Filter by return status

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 }
  }
}

Required permission: returns:read

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

Return counts

Returns the count of returns grouped by status.

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

Required permission: returns:read

Count groups not confirmed

The endpoint exists and returns a count per return status group, but the exact names of those groups were not confirmed in this audit (we don't assume they match the order status groups — pending, preparing, completed, cancelled, problematic, all — because they are different catalogs). Verify the exact shape against a real response before parsing it by field name.

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

List returns for an order

Lists all returns registered on a specific order.

orderIdstringrequired

Order ID

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

Required permission: returns:read

Create a return

Creates a return on an order.

orderIdstringrequired

Order ID

itemsarrayrequired

Order items to return, identified by sku (not by index). Minimum 1 element: { sku, quantity, reason? }.

reasonstringrequired

Reason for the return (see catalog below).

typestring

Return type: refund, exchange, or store-credit. Not enforced at runtime (see note below).

requestedBystring

Who requests the return: customer, seller, platform, or marketplace. Default: seller.

initialStatusstring

Initial status of the return (for manual returns already created in an advanced state, e.g. return-received).

overallConditionstring

Overall condition of the merchandise, if already known when creating (see catalog in the receive section).

shipmentInfoobject

Return shipping information, if already known when creating (manual returns).

{
  "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"
  }
}

Required permission: returns:create

Real catalog of reason (10 values)

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

Hyphenated, not underscored

All reason values use a hyphen (wrong-item, not wrong_item). Sending an underscored value fails validation.

items is identified by sku, not by itemIndex

Unlike POST /orders/{orderId}/prepare (which identifies items by itemIndex), creating a return identifies each item by sku: the service looks up that SKU inside order.items and fails with OrderValidationError if it isn't found, or if quantity exceeds the order's original quantity.

type isn't enforced at runtime despite the TypeScript type marking it required

The transport validator (createReturnSchema) only requires items and reason — it doesn't validate type at all. And at the library level, createReturn() assigns newReturn.type = input.type directly, with no check whatsoever: if you omit type, the field stays undefined in the saved document, with no error. The CreateReturnInput TypeScript interface marks type as non-optional, but that does NOT translate into an actual validation in any layer — it's a type promise the code doesn't keep. If your integration depends on every return having a type, send it explicitly; don't assume a default.

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

Get a return

Gets the detail of a specific return.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

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

Required permission: returns:read

Update a return

Updates fields of an existing return. At least one field is required in the body.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

statusstring

New status of the return.

overallConditionstring

Overall condition of the merchandise (see catalog in the receive section).

inspectionNotesstring

Notes from the merchandise inspection.

rejectionReasonstring

Rejection reason, if applicable.

skipMerchandiseReturnboolean

Whether the physical return of the merchandise is skipped (e.g. refund without return).

inventoryDispositionobject

What to do with the returned inventory: { locationId, locationName?, placeType: 'available'|'reconditioning'|'damaged'|'shrinkage', placeCode?, costImpact?, currency? }.

refundobject

Associated refund data: { amount, currency, method, status, transactionId? }.

shipmentInfoobject

Return shipment information: { 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"
  }
}

Required permission: returns:update

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

Approve a return

Approves a pending return. No body required.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

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

Required permission: returns:approve

No body required

The handler doesn't read any field from the body — send {} or simply omit the body.

Reject a return

Rejects a pending return, with a mandatory reason.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

reasonstringrequired

Reason for the rejection (free text, not a domain enum).

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

Required permission: returns:approve

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

Register the return shipment

Registers that the customer shipped the return merchandise back.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

trackingNumberstringrequired

Tracking number of the return shipment.

carrierstringrequired

Carrier code.

waybillIdstring

Waybill ID.

labelUrlstring

URL of the PDF label.

costobject

Cost of the return shipment: { labelCost, feniciaFee?, paidBy: 'customer'|'seller', currency }.

originobject

Origin address of the return shipment.

destinationobject

Destination address of the return shipment.

{
  "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" }
  }
}

Required permission: returns:update

Receive the returned merchandise

Registers the receipt of the returned merchandise, with its condition.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

conditionstringrequired

Condition of the merchandise received (see catalog below).

notesstring

Notes on the receipt.

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

Required permission: returns:process

Real catalog of condition (7 values)

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

Hyphenated, not underscored

No condition value matches new/used/damaged/unusable — those don't exist in the real catalog. Use exactly one of the 7 hyphenated values listed above.

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

Process the refund

Processes the refund for a return already received/inspected.

orderIdstringrequired

Order ID

returnIdstringrequired

Return ID

amountnumberrequired

Amount to refund (> 0).

currencystringrequired

3-letter currency (e.g. MXN).

methodstringrequired

Refund method (see catalog below).

transactionIdstring

Transaction ID of the payment method, if applicable.

{
  "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" } }

Required permission: returns:process

Real catalog of method (exactly 3 values)

original-payment, store-credit, bank-transfer

Hyphenated, exactly 3 values

The real catalog has exactly these 3 hyphenated values. There is no generic fourth value ("etc."); sending original_payment (underscore) fails validation.

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

Errors

CodeStatusDescription
returns:not_found404No return with that ID exists on that order.
returns:already_approved409The return has already been approved.
returns:already_rejected409The return has already been rejected.
returns:invalid_state422The operation is not valid in the current state of the return.
returns:items_required400The body doesn't include items (or the array is empty).
returns:refund_failed502The refund could not be processed against the payment method.
returns:already_processed409The return has already been processed previously.
orders:not_found404No order with that ID exists in your tenant.
auth:permission_denied403The API key doesn't have the required permission.
auth:invalid_token401The API key is invalid or was revoked.

Next steps