Order lifecycle — inbound receiving
Inbound receiving is the receive-only path through the Loca.fi
order state machine:
Open → Receiving → Received → Completed. It applies to the Inbound,
Replenishment, and Return order types — none of them has an allocate or
dispatch leg. This guide walks that one path end-to-end; the allocate/dispatch
side (Outbound, Transfer, Loan) is covered by
Order lifecycle — outbound & transfers.
Overview
The happy path is four calls:
- Create the order with its expected lines —
POST api/orders/createorder(OrderType: "Inbound", aToPlaceId,OrderSkuswithRequiredCount). - Scan arriving stock —
POST api/orders/addsnapshot. The first snapshot automatically moves the orderOpen → Receiving; tags the platform has never seen auto-create items against the matching SKU. - Receive —
POST api/orders/receiveorder. A stage gate checks every line's quantity constraint and that no error tags remain. - Complete —
POST api/orders/completeorder. Terminal.
Between scanning and receiving you may need to resolve errors: remove bad
scans (a Remove snapshot) or key in receipts by hand
(manualfulfillment). Every step writes an
OrderTransactionHistory row, so the whole story is reconstructable from
getordertransactions.
All order endpoints are gated on the Order module permission
(createorder and appendtags need Create; the lifecycle operations need
Update).
Prerequisites
- A token with
Ordercreate + update permission — see Getting started. - The destination place (
ToPlaceId) exists and is visible to the caller — inbound-type orders validateToPlaceId, notFromPlaceId. - Every
OrderSkusline references an operational SKU (Draft or Discontinued SKUs are rejected on new lines). - Skim Order lifecycle for the full state machine — this guide walks one path through it.
Step-by-step
1. Create the inbound order
curl -sk -X POST "http://<host>/api/orders/createorder" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderType":"Inbound",
"CustomerOrderNumber":"PO-2026-0142",
"ToPlaceId":"<warehouseId>",
"ExpectedArrivalDate":"2026-07-10T00:00:00Z",
"OrderSkus":[
{"SkuId":"<milkSkuId>","RequiredCount":24},
{"SkuId":"<breadSkuId>","RequiredCount":12,
"OrderQtyConstraintAction":"UnderOnly"}]}'
Returns 200 with the new order's OrderDetailDto — OrderState is Open.
Capture the Id.
Per line, RequiredCount is measured in outer-SKU units and
OrderQtyConstraintAction sets the receive gate for that line: Exact
(±0.01), OverOnly, UnderOnly, or All (any quantity). Omit it to use the
order-level setting. OrderUniqueItems ({ ItemId } rows) declares specific
items expected on the order, alongside or instead of quantity lines.
The pricing fields on each line — UnitPriceSnapshot, UnitCostSnapshot,
LineDiscount (must be ≥ 0) — and the order's Channel
(InStore/Online/Marketplace) are retail concerns. When the
snapshots are omitted the server auto-captures them from the SKU's current
pricing at line creation. Asset-management tenants can ignore all four.
2. Scan arriving stock
Feed reader/handheld scans to
POST api/orders/addsnapshot:
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-10T09:00:00Z",
"EndTime":"2026-07-10T09:02:30Z",
"Tags":[
{"TagNumber":"3034F8C000000000000001A1","TagType":"PassiveRfid",
"ReadCount":14,"Rssi":-52,"LastReadTime":"2026-07-10T09:01:12Z"}]}'
Returns 200 with an OrderOperationResultDto whose OrderDto carries the
updated order. On the first snapshot the order moves Open → Receiving
automatically — there is no explicit "start receiving" call.
What each scan does:
- A tag resolving to a known item marks it received against its SKU line
(
ReadOrderSkuDto.ReceivedCountandReceivedItemsupdate on the detail), moves the item to the order's ToPlace, and sets its item state toReceived(Restoredon Return orders — the only exit fromConsumed). - An unknown tag auto-creates an item for the matching SKU line — inbound receipts are how new stock enters Loca.fi.
- A scan that violates a constraint (wrong SKU, quantity overrun, …) records
an error tag instead: the order's
OrderStatusflips toErroredand the detail'sOrderErrorslists each violation.
Repeat with more snapshots as stock arrives — the order stays in Receiving.
The addsnapshot/{ignoreExisting}
variant controls whether tags already sitting at the snapshot place are
skipped (the base route defaults it to true; it applies to receiving scans
only and is forced off under a Strict receipt constraint).
3. Check progress and resolve errors
Poll the detail while receiving:
curl -sk "http://<host>/api/orders/getorder/<orderId>" \
-H "Authorization: Token $TOKEN"
Watch OrderSkuList[].ReceivedCount against RequiredCount, and
OrderErrors (each row carries the offending SKU/item plus violation flags —
IsQtyViolation, IsOrderSkuViolation, IsItemStateViolation, …).
OrderStatus: "Errored" blocks Complete, but not the operations that can
fix the errors. Two repair tools:
Remove bad scans — post a snapshot with SnapshotType: "Remove"
containing the tags to back out (allowed only while the order is in
Receiving). Each removed item reverts to the state and place captured when
it was scanned:
curl -sk -X POST "http://<host>/api/orders/addsnapshot" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>","PlaceId":"<warehouseId>",
"SnapshotType":"Remove",
"StartTime":"2026-07-10T09:30:00Z","EndTime":"2026-07-10T09:30:10Z",
"Tags":[{"TagNumber":"3034F8C000000000000001A1"}]}'
Manual fulfillment — key in receipts without a physical scan via
POST api/orders/manualfulfillment. The
server synthesises a snapshot from the tag numbers you supply:
curl -sk -X POST "http://<host>/api/orders/manualfulfillment" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>",
"ManualFulfillmentType":"ManualReceive",
"FulfillmentTags":[
{"TagNumber":"3034F8C000000000000001B2",
"ManualTagProcessingType":"Present"},
{"TagNumber":"3034F8C000000000000001C3",
"ManualTagProcessingType":"Missing"}]}'
Present records a normal receipt; Missing records the receipt and then
marks the item Missing — use it to acknowledge a line item that never
arrived. Manual fulfillment deliberately does not auto-Receive the order,
so you can repeat it as many times as needed before step 4.
4. Receive the order
curl -sk -X POST "http://<host>/api/orders/receiveorder/<orderId>" \
-H "Authorization: Token $TOKEN" -w "\nHTTP:%{http_code}\n"
(Equivalently POST api/orders/receiveorder with a ProcessOrderDto body —
{"OrderId":"<orderId>"}.) The stage gate
passes only when the order has at least one snapshot, every line's quantity
constraint is satisfied, and no error tags remain (error tags are
re-evaluated first — items that now pass are promoted). On success the order
moves to Received, ReceivedOn is stamped, and any expected-but-unreceived
items the order had claimed are swept to Missing.
receiveorder and completeorder return HTTP 200 even when the transition
is refused — the body is the order detail containing the errors. Never
trust the status code alone: compare the returned OrderState against what
you expected and inspect OrderErrors. A failed gate also logs a
ReceiveFailed transaction. See
Errors & response conventions.
Received too early, or need more scans? Wind the state back with
POST api/orders/revertordertoreceiving/{id}
(allowed only from Received), scan again, then re-Receive.
5. Complete the order
curl -sk -X POST "http://<host>/api/orders/completeorder/<orderId>" \
-H "Authorization: Token $TOKEN"
Moves Received → Completed (terminal) and stamps CompletedOn. A
still-Errored order is refused up front with a 400 InvalidOperation. To
abandon instead, POST api/orders/cancelorder/{id}
works from any non-terminal state — received items revert to Present,
un-received ones go to Missing.
6. Async variant for large receipts
addsnapshot, appendtags, receiveorder, and completeorder all accept
the async pattern: add
NotifyOnceComplete: true and set NotificationRecipientId to your own
user id (any other id silently runs the request synchronously):
curl -sk -X POST "http://<host>/api/orders/receiveorder" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>",
"NotifyOnceComplete":true,
"NotificationRecipientId":"<yourUserId>"}'
The response is an immediate 200 with
OrderOperationResultDto.BackgroundTaskId (and no OrderDto) — it is an
acknowledgement, not the outcome. Poll
GET api/backgroundtasks/GetBackgroundTask/{id}
until the task reaches Complete/Errored, or consume the auto-created
completion-event subscriptions. Then fetch the order detail to see the real
state.
Ad-hoc receiving with appendtags
When there is no purchase order to receive against, skip the pre-defined
lines entirely: create a bare order (step 1 without OrderSkus) and grow it
from the scans themselves with
POST api/orders/appendtags. The
body extends the snapshot DTO with a ProcessAs mode (integer-serialized:
0 = Unique item lines, 1 = OuterSkus, 2 = InnerSkus):
curl -sk -X POST "http://<host>/api/orders/appendtags" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>","PlaceId":"<warehouseId>",
"SnapshotType":"Add","ProcessAs":1,
"StartTime":"2026-07-10T10:00:00Z","EndTime":"2026-07-10T10:01:00Z",
"Tags":[{"TagNumber":"3034F8C000000000000001D4"}]}'
Each scan creates or increments the matching order line as it records the
receipt (under OuterSkus, an inner-SKU scan accrues 1/InnerQuantity
against the outer line). Unknown tags auto-create items here too. Note
appendtags requires the Order Create permission, not Update. The
Receive/Complete steps are unchanged.
Audit trail
Reconstruct who scanned what, when:
curl -sk "http://<host>/api/orders/getordertransactions/<orderId>" \
-H "Authorization: Token $TOKEN"
Returns every transition — Created, Snapshot, ManualReceive,
ReceiveFailed, Received, Completed, … — each with the acting user or
agent, timestamp, the snapshot place where relevant, and per-item
TransactionRecords (tag number, item, and per-violation flags). For
cross-order reporting use the OData-filterable flattened view,
GET api/orders/getfilteredordertransactions.
Error handling
All failures follow the platform-wide
error and response conventions —
400 with a JSON array of { ErrorCode, Message, Field } — except where
noted.
| Situation | Response |
|---|---|
Missing/unparseable OrderType on create | 400 MissingRequiredField / InvalidOperation |
ToPlaceId missing or not visible (inbound types) | 400 InvalidReference, field ToPlaceId |
OrderSkus line with an unknown SKU, or a new line on a non-operational (Draft/Discontinued) SKU | 400 |
Negative LineDiscount, or Channel not a valid name | 400 InvalidOperation |
Lifecycle call while another operation holds the order's lock, or Complete while Errored | 400 InvalidOperation with "OrderState => X|OrderStatus => Y" |
Snapshot/appendtags in a non-scanning state (Received/Completed/Cancelled) | 400 OperationFailed (not a clean invalid-state message) |
| Receive gate fails (quantity constraint / error tags) | 200 with unchanged OrderState + ReceiveFailed transaction |
| Complete/Receive on an unknown order id | 400 NotFound |
getorder with an unknown id | 400 [{"ErrorCode":"NotFound","Field":"Id"}] |
getordertransactions with an unknown id | 200 [] |
| Manual fulfillment when the order's ToPlace cannot be resolved | 400 "Manual fulfillment requires Order To Place" |
| Async scheduling failure | 400 OperationFailed |
No token / wrong scheme (Bearer) | 401; token without the Order permission → 403 |
Related
- Order lifecycle — the full state machine, order types, and item-state cascades.
- Order lifecycle — outbound & transfers — the allocate/dispatch side of the machine.
- Async operations — the
NotifyOnceComplete/ background-task pattern in depth. - Item lifecycle — the item states receiving
scans set (
Received,Restored,Missing). - Reference pages: Add Order, Add Snapshot, Append Tags, Manual Receive, Receive, Complete, Revert To Receiving, Cancel Order, Get Order, Get Order Transactions.