Skip to main content

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:

  • OutboundOpen → Allocating → Allocated → Dispatched → Completed. Allocate leg only; Complete is called straight from Dispatched.
  • TransferOpen → 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:

  1. Create the order with its SKU/item lines — POST api/orders/createorder.
  2. Scan the pickPOST api/orders/addsnapshot uploads reads; the first scan auto-moves the order Open → Allocating and each valid item to state Allocated at the order's FromPlace.
  3. Pass the Allocate gatePOST api/orders/allocateorder checks quantity constraints and error tags; success → Allocated.
  4. DispatchPOST api/orders/dispatchorderDispatched, items → Dispatched.
  5. Finish — Outbound: completeorder directly. Transfer: scan at the destination (auto Dispatched → Receiving), receiveorder, then completeorder.

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 Order Create + 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 Draft or Discontinued SKU 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.

200 despite failure

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.

Retail vs Asset

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):

SettingValuesEffect
EnableOrderFromPlaceConstraint (+ IncludeOrderFromPlaceChildren)boolOnly tags scanned from the order's FromPlace (optionally including child places) allocate cleanly.
OrderFromPlaceActionErrorOut / IgnoreWhat a from-place violation does: record an error tag that blocks the gates, or just log a transaction.
OrderReceiptConstraintStrict / AnyStrict: only tags allocated on this order can be received on the receive leg.
OrderQtyConstraintActionExact / OverOnly / UnderOnly / AllQuantity rule per line at the Allocate/Receive gates (outer-SKU units; Exact allows ±0.01). No organisation fallback — settable per line too.
EnableCustomPropertyConstraintboolEnforce per-line extended-property constraints (AddOrderSkuDto.ExtendedProperties) against scanned items.

Loan orders

Retail vs Asset

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 conventions400 with a JSON array of { ErrorCode, Message, Field }except the lifecycle actions' 200-despite-failure behaviour called out above.

SituationResponse
Missing/unknown OrderType, missing required place for the type, unknown CustomerId/DeliverToPersonId/SKU/item400 naming the field (MissingRequiredField/InvalidReference)
New SKU line referencing a non-operational (Draft/Discontinued) SKU400 InvalidOperation
Negative LineDiscount on a line400 InvalidOperation, field LineDiscount
Lifecycle call with no order id in URL or body400 MissingRequiredField ("Order Id not set")
Operation not allowed in the current state (e.g. addsnapshot on an Allocated order), or the order is mid-processing400 InvalidOperation with "OrderState => …|OrderStatus => …" (non-scanning states may surface as OperationFailed)
Allocate/Dispatch/Receive/Complete transition refused or gate failed200 with unchanged OrderState + populated OrderErrors — diff, don't trust the status code
Revert from the wrong state400 CannotCompleteOrder (same code for both revert endpoints)
Cancel a Completed/Cancelled order400 CannotCancelOrder
Unknown order id on getorder400 NotFound, field Id
Async scheduling failure (NotifyOnceComplete)400 OperationFailed
No token / wrong scheme (Bearer)401