Order lifecycle — outbound & transfers
Outbound and Transfer orders are Loca.fi's stock-leaving flows: pick items at a source place, verify the pick against the order lines, dispatch — and, for transfers, scan the stock back in at the destination. This guide walks the allocate leg end to end; the receive leg is shared with inbound receiving, which covers it in depth.
Read Order lifecycle first for the full state machine — this guide is the doing, that page is the understanding.
Overview
The flow by order type:
- Outbound —
Open → Allocating → Allocated → Dispatched → Completed. Allocate leg only; Complete is called straight fromDispatched. - Transfer —
Open → Allocating → Allocated → Dispatched → Receiving → Received → Completed. Both legs. - Loan — same shape as Transfer, with loan item states
(
Reserved/OnLoan/Returned) and reversed place moves — see Loan orders below.
The moves are:
- Create the order with its SKU/item lines —
POST api/orders/createorder. - Scan the pick —
POST api/orders/addsnapshotuploads reads; the first scan auto-moves the orderOpen → Allocatingand each valid item to stateAllocatedat the order's FromPlace. - Pass the Allocate gate —
POST api/orders/allocateorderchecks quantity constraints and error tags; success →Allocated. - Dispatch —
POST api/orders/dispatchorder→Dispatched, items →Dispatched. - Finish — Outbound:
completeorderdirectly. Transfer: scan at the destination (autoDispatched → Receiving),receiveorder, thencompleteorder.
All lifecycle endpoints are gated on the Order module permission
(createorder needs Create, the rest Update). Every transition writes an
audit row readable via
GET api/orders/getordertransactions/{id}.
Prerequisites
- A token with
OrderCreate + Update permission — see Getting started. - The source place (Outbound) or source and destination places (Transfer/ Loan) — the caller must have visibility of them.
- Operational SKUs for every SKU line (a
DraftorDiscontinuedSKU on a new line is rejected), and tagged items of those SKUs at the source place.
Step-by-step
1. Create the order
Outbound requires FromPlaceId; Transfer and Loan require both FromPlaceId
and ToPlaceId. Lines are quantity-based SKU lines (OrderSkus) and/or
specific-item lines (OrderUniqueItems):
curl -sk -X POST "http://<host>/api/orders/createorder" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderType":"Transfer",
"CustomerOrderNumber":"TRF-0042",
"FromPlaceId":"<warehouseId>","ToPlaceId":"<storeId>",
"OrderSkus":[{"SkuId":"<milkSkuId>","RequiredCount":10}],
"OrderUniqueItems":[{"ItemId":"<specificItemId>"}]}'
Returns 200 with the OrderDetailDto — state Open, status Ready.
Capture the Id. RequiredCount is measured in outer-SKU units; a
per-line OrderQtyConstraintAction (Exact, OverOnly, UnderOnly, All)
overrides the order-level setting at the stage gates.
2. Upload allocation scans
Scan the picked stock at the source and post the reads. The first snapshot
automatically moves the order Open → Allocating; each item that passes the
order's constraints becomes state Allocated and is moved to the FromPlace:
curl -sk -X POST "http://<host>/api/orders/addsnapshot" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>","PlaceId":"<warehouseId>",
"SnapshotType":"Add",
"StartTime":"2026-07-07T09:00:00Z","EndTime":"2026-07-07T09:02:00Z",
"Tags":[{"TagNumber":"3034F4A1B2C3D4E5F6A7B8C9","TagType":"PassiveRfid",
"ReadCount":12,"Rssi":-52,
"LastReadTime":"2026-07-07T09:01:30Z"}]}'
Returns 200 with OrderOperationResultDto — the refreshed detail in
OrderDto. Repeat as many times as the pick needs. Check OrderDto.OrderErrors
after each upload: a tag that violates a constraint (wrong from-place, over
quantity, item state not allowed, …) is recorded as an error tag and
flips OrderStatus to Errored.
To undo mistaken scans, post the same tags with "SnapshotType":"Remove" —
each item reverts to its pre-scan state and place. Removal is only allowed
while the order is in Allocating (or Receiving on the receive leg).
3. Pass the Allocate gate
curl -sk -X POST "http://<host>/api/orders/allocateorder/<orderId>" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d 'null'
The gate passes only when the order has at least one snapshot, every line's
quantity constraint is met, and no error tags remain (error tags are
re-evaluated first — items that now pass are promoted). Success →
state Allocated; failure logs an AllocateFailed transaction and the order
stays Allocating.
allocateorder, dispatchorder, receiveorder and completeorder return
HTTP 200 with the order detail even when the transition is refused —
the detail carries the errors. Always diff the returned OrderState against
what you expected and inspect OrderErrors/OrderStatus. See
Errors & response conventions.
Sending a JSON ProcessOrderDto body instead of null wraps the response in
OrderOperationResultDto and unlocks the async variant: set
NotifyOnceComplete: true with NotificationRecipientId equal to your own
user/agent id to get back a BackgroundTaskId immediately — see
Async operations. (A recipient id that
isn't yours silently runs the request synchronously.)
4. Dispatch
curl -sk -X POST "http://<host>/api/orders/dispatchorder/<orderId>" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d 'null'
Allocated → Dispatched; every allocated item becomes state Dispatched
(standard orders do not move items at dispatch — the place move happens
at the receiving scan), and DispatchedOn is stamped. Dispatch is blocked
while OrderStatus is Errored.
Second thoughts after allocating? POST api/orders/revertordertoallocating/{id}
drops the order back to Allocating so you can add or remove scans — allowed
only from Allocated.
5. Outbound: complete
For an Outbound order there is no receive leg — complete straight from
Dispatched:
curl -sk -X POST "http://<host>/api/orders/completeorder/<orderId>" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d 'null'
Dispatched → Completed (terminal); CompletedOn is stamped.
For retail tenants an Outbound completion is a sale signal: it stamps
LastSoldAt on the dispatched items and their SKUs (as does any completion
whose destination place is of type Sold). Asset-tracking tenants use
Outbound simply as stock-leaving; the stamps are inert for them.
6. Transfer: receive at the destination and complete
A Transfer continues at the destination. Scanning there with
addsnapshot (same payload as step 2, with
PlaceId = the destination) auto-moves the order Dispatched → Receiving;
each scanned item becomes Received and is moved to the ToPlace. Then close
the leg:
curl -sk -X POST "http://<host>/api/orders/receiveorder/<orderId>" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d 'null'
# then
curl -sk -X POST "http://<host>/api/orders/completeorder/<orderId>" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d 'null'
The Receive gate mirrors Allocate (quantities met, no error tags). On
success, items that were dispatched but never scanned in are swept to
Missing at the destination, and ReceivedOn is stamped. completeorder
then takes Received → Completed. revertordertoreceiving
(allowed only from Received) reopens the receive leg for more scans. The
receive leg's error handling, ignore-existing behaviour and manual
fulfilment are covered in the inbound receiving guide
— everything there applies to a Transfer's receive leg too.
Variants
One-shot dispatch: AutoReceiptOrder
Create a Transfer (or Loan) with "AutoReceiptOrder": true (or set the
organisation default) and the server runs the entire receive leg for you at
dispatch: a receive snapshot is auto-generated from all allocated tags at the
destination, then the order is auto-received and auto-completed. One
dispatchorder call, done. Useful when the destination has no scanning
hardware and the pick is trusted.
Cancelling
POST api/orders/cancelorder/{id} is
allowed from every non-terminal state and reverts items according to how far
the order got: from Allocating/Allocated items return to Present; from
Dispatched they become Missing; from Receiving/Received, received
items go to Present and un-received ones to Missing. Cancelling a
Completed or already-Cancelled order returns 400 (CannotCancelOrder).
Constraint modes
Set on the order at create (falling back to organisation defaults when omitted):
| Setting | Values | Effect |
|---|---|---|
EnableOrderFromPlaceConstraint (+ IncludeOrderFromPlaceChildren) | bool | Only tags scanned from the order's FromPlace (optionally including child places) allocate cleanly. |
OrderFromPlaceAction | ErrorOut / Ignore | What a from-place violation does: record an error tag that blocks the gates, or just log a transaction. |
OrderReceiptConstraint | Strict / Any | Strict: only tags allocated on this order can be received on the receive leg. |
OrderQtyConstraintAction | Exact / OverOnly / UnderOnly / All | Quantity rule per line at the Allocate/Receive gates (outer-SKU units; Exact allows ±0.01). No organisation fallback — settable per line too. |
EnableCustomPropertyConstraint | bool | Enforce per-line extended-property constraints (AddOrderSkuDto.ExtendedProperties) against scanned items. |
Loan orders
Loans are an asset-management flow — lending equipment out and getting the same items back. Retail tenants typically won't use them.
A Loan runs the full Transfer-shaped chain but with loan item states and reversed place moves:
- Allocating scan → item state
Reserved(moved to the FromPlace, as usual). - Dispatch →
OnLoan, and — unlike standard orders — the items are moved to the ToPlace at dispatch (they're now with the borrower). - Receiving scan (the return) →
Returned, items moved back to the FromPlace. - Complete: any items still on loan and never scanned back become
Missing.
The API calls are identical to the Transfer sequence above — only
"OrderType": "Loan" changes.
Error handling
All failures follow the platform-wide
error and response conventions —
400 with a JSON array of { ErrorCode, Message, Field } — except the
lifecycle actions' 200-despite-failure behaviour called out above.
| Situation | Response |
|---|---|
Missing/unknown OrderType, missing required place for the type, unknown CustomerId/DeliverToPersonId/SKU/item | 400 naming the field (MissingRequiredField/InvalidReference) |
New SKU line referencing a non-operational (Draft/Discontinued) SKU | 400 InvalidOperation |
Negative LineDiscount on a line | 400 InvalidOperation, field LineDiscount |
| Lifecycle call with no order id in URL or body | 400 MissingRequiredField ("Order Id not set") |
Operation not allowed in the current state (e.g. addsnapshot on an Allocated order), or the order is mid-processing | 400 InvalidOperation with "OrderState => …|OrderStatus => …" (non-scanning states may surface as OperationFailed) |
| Allocate/Dispatch/Receive/Complete transition refused or gate failed | 200 with unchanged OrderState + populated OrderErrors — diff, don't trust the status code |
| Revert from the wrong state | 400 CannotCompleteOrder (same code for both revert endpoints) |
Cancel a Completed/Cancelled order | 400 CannotCancelOrder |
Unknown order id on getorder | 400 NotFound, field Id |
Async scheduling failure (NotifyOnceComplete) | 400 OperationFailed |
No token / wrong scheme (Bearer) | 401 |
Related
- Order lifecycle — the full state machine, per-state operation matrix and item-state cascades.
- Order lifecycle — inbound receiving — the receive leg in depth (error tags, manual fulfilment, ignore-existing).
- Item lifecycle — the item-side states the order operations set.
- Async operations —
NotifyOnceComplete, background tasks and completion events. - Reference pages: Create Order, Add Snapshot, Allocate, Dispatch, Receive, Complete, Cancel Order, Revert to Allocating, Revert to Receiving, Get Order, Order Transactions.