Inventory transfers

Transfers move stock from an origin location to a destination location. All endpoints for this resource require the inventory:transfer permission.

Read the write semantics section before integrating

The exact moment stock moves, and the guarantees (or lack thereof) about a transfer's state, are the most important detail of this resource. They are described below with the real production behavior — this is not the ideal design, it is what the system does today.

List transfers

Lists the tenant's transfers, with optional filters

pagenumber

Page, 1-indexed. Default: 1.

limitnumber

Items per page. Default: 20.

statusstring

Filter by status: pending, approved, received, cancelled.

originIdstring

Filter by origin location.

destinationIdstring

Filter by destination location.

200
{
  "transfers": [
    {
      "_id": "66f1a2b3c4d5e6f7a8b9c0d1",
      "originId": "loc_abc123",
      "destinationId": "loc_def456",
      "status": "pending",
      "productsList": [
        { "sku": "CAM-ROJO-M", "reassignAmount": 10, "originAmount": 42 }
      ],
      "createdBy": "usr_789"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 12, "pages": 1 }
}

Query a transfer

Queries a transfer by ID

idstringrequired

Transfer ID.

200
{ "_id": "66f1a2b3c4d5e6f7a8b9c0d1", "originId": "loc_abc123", "destinationId": "loc_def456", "status": "pending" }
404
{ "code": "not-found" }

Create a transfer

Creates a transfer in 'pending' status. Does NOT move stock yet.

originIdstringrequired

Origin location.

destinationIdstringrequired

Destination location.

productsListarrayrequired

Non-empty list of { sku, reassignAmount, originAmount }.

{
  "originId": "loc_abc123",
  "destinationId": "loc_def456",
  "productsList": [{ "sku": "CAM-ROJO-M", "reassignAmount": 10, "originAmount": 42 }]
}
201
{
  "_id": "66f1a2b3c4d5e6f7a8b9c0d1",
  "originId": "loc_abc123",
  "destinationId": "loc_def456",
  "productsList": [{ "sku": "CAM-ROJO-M", "reassignAmount": 10, "originAmount": 42 }],
  "status": "pending",
  "createdBy": "usr_789"
}
400
{ "code": "bad-request/missing-origin" }

Creating only records intent

POST /inventory/transfers does not move stock. It only creates the record with status: 'pending'. Stock moves exclusively when the transfer is received (see below).

Approve

Marks the transfer as approved

idstringrequired

Transfer ID.

No body required — the handler never reads event.body.

{}
200
{ "_id": "66f1a2b3c4d5e6f7a8b9c0d1", "status": "approved", "approvedBy": "usr_789", "approvedAt": "2026-06-01T10:00:00.000Z" }
404
{ "code": "not-found" }

Receive (moves the stock)

Applies the stock movement: decrements origin, increments destination

idstringrequired

Transfer ID.

receivedItemsarray

Optional list of { sku, reassignAmount, originAmount } to receive. If omitted, the transfer's original list is used.

{
  "receivedItems": [{ "sku": "CAM-ROJO-M", "reassignAmount": 10, "originAmount": 42 }]
}
200
{ "_id": "66f1a2b3c4d5e6f7a8b9c0d1", "status": "received" }
404
{ "code": "not-found" }

This is the only thing that moves stock in a transfer's lifecycle. For each item received:

  1. The origin stock is decremented by reassignAmount, with the result floored at a minimum of 0if the amount exceeds the stock available at origin, the subtraction saturates at 0 instead of rejecting the operation. No visible error is produced for this over-send.
  2. The destination stock is incremented by the same reassignAmount.
  3. Internally, the system attempts to consume/create FIFO lots to maintain cost traceability; if that part fails, the receive does not fail — the lot error is logged as a warning and the request still responds 200.

Receiving does NOT emit the 'Inventory Updated' event

Unlike a direct stock write (PUT, bulk-update, adjustments), receiving a transfer moves stock through a different path that does not trigger the Inventory Updated event nor the low-stock check. If your integration listens for that event to react to stock changes, it will not see the movements caused by received transfers.

No state-transition guard — a transfer can be received twice

There is no check that the transfer is in the correct state before approving, receiving, or cancelling it. This means, in current production behavior:

  • You can call receive on a transfer that is already received — the stock movement gets reapplied, duplicating the effect.
  • You can cancel a transfer that was already received.
  • The receive operation is not idempotent: resending the same request reapplies the stock delta every time, because there is no deduplication key.

Your integration is responsible for not blindly retrying receive and for tracking the state locally before calling it again.

Cancel

Marks the transfer as cancelled

idstringrequired

Transfer ID.

cancellationReasonstring

Cancellation reason (or the alternate field `reason`).

{ "cancellationReason": "Error al capturar destino" }
200
{ "_id": "66f1a2b3c4d5e6f7a8b9c0d1", "status": "cancelled" }
404
{ "code": "not-found" }

Cancelling does not revert any stock movement already applied by a previous receive — it only changes the status field. If you already received the transfer and then cancel it, the moved stock stays moved.

Example — full cycle

# 1. Create
curl -X POST 'https://api.fenicia.io/inventory/transfers' \
  -H 'Authorization: Bearer fkapi_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "originId": "loc_abc123",
    "destinationId": "loc_def456",
    "productsList": [{ "sku": "CAM-ROJO-M", "reassignAmount": 10, "originAmount": 42 }]
  }'
 
# 2. Receive (moves the stock)
curl -X POST 'https://api.fenicia.io/inventory/transfers/66f1a2b3c4d5e6f7a8b9c0d1/receive' \
  -H 'Authorization: Bearer fkapi_your_api_key'

Errors

CodeStatusDescription
bad-request/missing-tenant401The tenant could not be resolved from the API key.
bad-request/missing-origin400originId is missing.
bad-request/missing-destination400destinationId is missing.
bad-request/missing-products400productsList is missing or empty.
not-found404No transfer exists with that ID.
internal-error500Internal error.

Next steps