Transfers
Move stock between locations with a structured request, approve, ship, and receive workflow — every unit tracked individually from source bin to destination bin. Includes sending stock out from a warehouse to one of your stores, and moving it between two areas of one location.
An inventory transfer is a formal request to move units from one location to another. Rather than silently teleporting stock, transfers give a source location the chance to approve and assign specific units, then let the destination location receive them into a bin — keeping the audit trail honest and the unit statuses accurate throughout the journey.
Transfers only appear in multi-location accounts. The Transfers menu item is hidden when your account has a single location, because moving stock between bins within one location is done directly on the unit.
Sending a truck of stock out to one of your stores #
Transfers are pull-shaped. The request is raised by the location that wants the stock rather than by the one sending it, because the store running low is the one that knows it is running low.
That is why a warehouse manager pushing stock out to a store hits a wall the first time. Nothing is missing here and nothing needs building — the permission she is short of is one at the store, not at her own warehouse. Look at what New Transfer asks for:
- To Location — the field her permission is checked against. She needs create inventory transfers at the destination. Locations she may not send to are still on the list, greyed out, each carrying its reason: that she holds no role there, or that the role she does hold there does not carry the permission. Saving one anyway is refused with the same sentence, and with where to go and ask.
- From Location — offers every location on the account, deliberately. Naming somebody else's warehouse is the whole point of the field, so it is never narrowed, and nothing is asked about it.
So what an administrator gives her is a role carrying create inventory transfers at each store she ships to, added under Location Assignments on her user record or from the role's own detail view — see location-scoped roles. A role that is account-wide already holds it everywhere and there is nothing to do.
With that in place she raises the transfer herself — From her warehouse, To the store, a line per product going on the truck — and then approves it herself on Approve / Assign Units, because approval belongs to the location giving the stock up. Raising and approving the same transfer is right here rather than a loophole: approval is consent to part with the stock, and on a push the party parting with it is the party asking for it.
Receive Units is then the store's step, and the one that matters to it. The goods are not the store's stock until somebody puts them on one of its shelves, and doing that is what closes the transfer.
Moving stock between two areas of one location #
If "store room" and "shop floor" are two areas of the same location, none of the above applies — that is not a transfer. They are two bins at one location, and bins nest, so Shop Floor > Aisle 3 > Shelf B and Store Room > Rack 1 can hang off the same location with as much structure underneath as you want.
Moving stock between them is a change to the unit rather than a document. Open the unit and use Transfer: leave Destination Location on None and pick the Destination Bin you want. To move a shelf's worth at once, tick the units on the Units list and use its Transfer bulk action, which asks the same two questions of the whole selection. Either way the move lands in each unit's movement history, so where a thing went is still on the record.
Both ask for update inventory items at the location the bin stands at — which, for a move inside one location, is the location you are already working at.
The transfer lifecycle #
A transfer moves through a fixed sequence of statuses:
- Draft — being built; no units are committed yet.
- Requested — the origin location has been notified. The transfer appears as a pending action for users at the source.
- Approved — a user at the source location has reviewed the request, assigned specific units to each line, and confirmed shipment.
- In Transit — the units are physically on the way.
- Delivered — the shipment has arrived at the destination and is ready to be received.
- Received — all units have been scanned or confirmed into a destination bin. Units land as In Stock at the new location.
- Cancelled — the transfer was rejected or abandoned. Cancellation is available from Draft, Requested, and Approved; once a transfer is In Transit or later it cannot be cancelled.
Each status transition is timestamped on the transfer record (requested_at,
approved_at, shipped_at, delivered_at, received_at).
Creating a transfer #
New transfers are created from Inventory → Transfers → New Transfer.
- From Location — where the stock is currently held. Required.
- To Location — where it needs to go, and the field your permission is checked against. Required. Moving stock between two areas of one location is not a transfer — see Moving stock between two areas of one location. The REST API and the assistant tools refuse a transfer whose destination is its own source; this form does not stop you saving one.
- Requested By — the user initiating the request. Optional; defaults to unknown.
- Status — starts as Draft. Change to Requested to notify the source location.
- Expected Delivery Date — an optional target date used for planning.
- Notes — free-text context for the approver.
- Requested Products (repeater) — one or more lines, each specifying a Product and a requested quantity. You can add as many lines as needed with Add Line.
Save the transfer in Draft to keep editing it, or set the status to Requested when it's ready for the source location to act on.
Approving a transfer #
When a transfer reaches Requested status, users at the source location see a badge on the Transfers menu item. Opening the transfer and clicking Approve / Assign Units opens a dedicated approval screen.
On the approval screen each requested line is shown alongside the available units at the source location, grouped by bin, lot, and expiration date. You assign units to each line by:
- Clicking individual units to toggle them in or out,
- Entering a quantity for a group to auto-select that many units from it, or
- Scanning barcodes — scan a bin barcode first to set context, then scan unit or Product barcodes to assign them one by one.
The screen shows whether each line is satisfied (assigned quantity meets the requested quantity) or still short. You can approve a transfer even if some lines are only partially filled — if the destination location has another source configured with auto-transfer enabled, the remaining shortfall is automatically routed to that source as a new Requested transfer.
Clicking Reject All cancels the transfer entirely and notifies the requester.
Once approved, the assigned units are placed On Hold at the source location and the transfer advances to Approved.
Shipping and delivery #
Once a transfer is Approved, press Mark Shipped as the units leave the source. The confirmation asks for a tracking number and an expected delivery date, both optional. The delivery date is also on the form when you raise the transfer, and shipping is usually when somebody actually knows it; the tracking number is on no form at all, so Mark Shipped is the only place it can be recorded.
When the shipment arrives, press Mark Delivered — this queues it for receiving.
Receiving a transfer #
When a transfer is Delivered, users at the destination location see it flagged for action. Opening it and clicking Receive Units opens the receiving screen.
Receiving needs update_inventory_transfers at the destination — see
Permissions below.
Before receiving any units, set the destination bin — either choose from the dropdown or scan a bin barcode. The location's default receiving bin is pre-selected if one is configured and it actually stands at that location — if the default has been pointed at another location's shelf, it is silently not offered, and you pick one yourself. Only bins at the transfer's destination are offered, and receiving into a bin anywhere else is refused: a bin belongs to a location, so choosing one at another location would move the stock there rather than shelve it where the truck actually arrived. Then receive units by:
- Scanning a unit barcode — receives that specific unit.
- Scanning a Product barcode — receives all in-transit units for that Product at once.
- Clicking Receive on an individual row.
- Receive Group — receives all in-transit units for a Product/lot/expiration group.
- Receive All — marks every remaining in-transit unit as received in one click.
Each received unit is moved to the selected destination bin, its status becomes In Stock, and a Transfer movement is written to its history. When every unit is received the transfer status automatically advances to Received.
Transfer lines and units #
A transfer has two levels of detail:
- Lines — one per Product, recording the
requested_quantityandapproved_quantity. Lines define what was asked for and what was committed. - Transfer Units — one per individual inventory unit selected during
approval, linked to a line. Each unit tracks its own status:
pending,approved,rejected,in_transit,received, ormissing.
Both are visible as tabs on the transfer's view page, alongside the full change history log.
Permissions and visibility #
Transfers respect per-location roles.
The location context filter narrows the list to transfers touching a specific
location. Creating one requires create_inventory_transfers at the To
Location — the location receiving the stock, whoever is doing the typing — and
nothing is asked about the source. Editing a Draft requires
create_inventory_transfers at its destination, and editing one at any later
status requires
update_inventory_transfers at whichever end owns that status — the destination
for Requested, Delivered, Received and Cancelled, the source for Approved,
In Transit, Delivered and Cancelled.
The transfer list narrows to transfers touching a location you hold a role at — either end counts, since a transfer you are sending is as much yours as one you are receiving. An account-wide role is not narrowed at all.
Beyond that, each step of a transfer belongs to one end of it, and the screens, the API and a connected assistant all ask the same question:
| Step | Belongs to | Permission |
|---|---|---|
| Submit Request | destination | create_inventory_transfers |
| Approve / Assign Units | source | update_inventory_transfers |
| Mark Shipped | source | update_inventory_transfers |
| Mark Delivered | either end | update_inventory_transfers or deliver_inventory_transfers |
| Receive Units | destination | update_inventory_transfers |
| Cancel | depends where it stands | create_ at the destination while Draft; either end with update_ while Requested; update_ at the source once Approved |
Cancelling follows the transfer rather than sitting at one end, and that is deliberate. A request nobody has approved yet may be ended by either end: the destination withdrawing it, or the source declining it — which is exactly what Reject All on the approval screen does. Once the source has approved it and set units aside, calling it off is the source's alone. Fixing cancellation to one end would have taken away either a store's ability to withdraw its own request or a warehouse's ability to turn one down.
Two limits worth knowing. Over the API, GET /api/v1/inventory/transfers lists
the transfers with either end at a location your role covers, the same answer the
screen gives — a role assigned to no location in particular, which is most
account-wide roles, still sees them all. Filter with from_location_id /
to_location_id if you want one location's. And Mark
Delivered over the API needs update_inventory_transfers: a role given only
the dedicated deliver_inventory_transfers permission can do it on the screen
but not through the API, because that permission does not open the endpoint.
Separately, the lifecycle is enforced — only a legal next step is accepted
from wherever the transfer stands, so rewinding a Delivered transfer to
requested, or cancelling one already in transit, is refused with a 422 whoever
asks. That is a different question from who may ask, and the two are checked
independently. Undo sidesteps the permission question but not the lifecycle
one. It restores a previous value without asking whether you own that step —
anyone holding update inventory transfers anywhere on the account can undo —
but an undo that would move a transfer's status backwards is refused exactly
like any other rewind.
Receiving being tied to the destination is newer than the rest of this page, and is the change most likely to be noticed: somebody who could previously open the receive screen for any transfer in the account now gets a refusal unless their role reaches the location the goods arrive at. That is deliberate. Booking in a delivery you are not standing next to marked the transfer Received while the stock never moved, because the units at a location you hold no role at are not visible to you — a closed transfer over a full lorry.
To Location offers every location, greying out the ones you may not transfer into and saying why on the option. It used to hide them, which left a destination silently missing from a six-location account. From Location is not narrowed at all, and deliberately: a transfer is raised by the location that wants the stock, so the source is nearly always somebody else's warehouse.
Doing it from the API #
# Create a transfer request with two lines
curl -X POST "https://your-domain.com/api/v1/inventory/transfers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from_location_id": "<source-location-id>",
"to_location_id": "<destination-location-id>",
"expected_delivery_date": "2026-06-15",
"notes": "Restocking for seasonal demand",
"lines": [
{"product_id": "<product-id>", "requested_quantity": 10},
{"product_id": "<another-product-id>", "requested_quantity": 5}
]
}'
# Advance the transfer to "in_transit"
curl -X POST "https://your-domain.com/api/v1/inventory/transfers/{id}/transition" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to": "in_transit"}'
to_location_id must name a different location from from_location_id; a
transfer from a location to itself is refused here with a 422. Filter the list by
status, from_location_id, or to_location_id as query parameters. Valid
to values for the transition endpoint are: requested, approved,
in_transit, delivered, received, and cancelled.