Order reservations
When a paid order routes to a location in a multi-location store, Shopify reserves the finished-good variant at that location — but the BOM components it would consume are still sitting in available. Until the order is actually fulfilled, another order can sell those same components out from under it. Order reservations close that window: at routing-complete, Assemblified moves the resolved component quantities from available → reserved at the routed location, and consumes them on fulfillment.
This is an opt-in shop-wide setting. Without it, components stay in available until fulfillment and are deducted then — the legacy multi-location behaviour.
On this page
Section titled “On this page”- What it does and why
- Prerequisites and how to enable
- The five Shopify webhooks that drive it
- Lifecycle states
- How each material kind behaves (Shopify, virtual, pre-assembled)
- Failure handling and retry
- How it differs from safety-stock reservations
- Edge cases & gotchas
What it does
Section titled “What it does”For every BOMBill of MaterialsA bill of materials tells Assemblified how to build one unit of a finished good. When a customer orders the finished-good variant, Assemblified deducts the right component quantities from inventory automatically. Read more → line item on an incoming order, Assemblified expands the recipe (recursing through sub-assemblies, applying waste percentages) to the flat list of raw materials and pre-assembled draw-down. When Shopify completes routing for the order’s fulfillment order, those resolved components are reserved at the routed location:
- Shopify-linked materials —
availabledrops,reservedrises by the same amount, viainventoryMoveQuantitiesagainst Shopify’s native buckets. - Virtual materials — Assemblified’s internal
availabledrops; an audit row records the reservation. - Pre-assembled stock — when a BOM component is itself a finished-good with a pre-assembled shelf, the shelf is drawn down first, then any remainder draws from raws.
When the order is fulfilled, the reserved quantities are consumed — reserved drops, the shelf doesn’t bounce back to available, and the inventory is gone for good. If the order is cancelled, refunded, or rerouted before fulfillment, the reservation is released — quantities return to available.
Why use it
Section titled “Why use it”The default multi-location behaviour deducts components at fulfillment, which means between order-paid and shipment the storefront still shows those components as sellable. In a fast-moving store this opens an over-promise window: a flurry of orders for two BOMs that share a bottleneck component can all succeed at checkout, then collide at fulfillment.
Turning on order reservations trades visible storefront stock against safety. The tradeoff:
| With reservations | Without reservations |
|---|---|
Component available drops at routing-complete (typically right after payment). | Component available drops only at fulfillment. |
| Storefront reflects the true buildable count immediately. | Storefront may temporarily over-promise during the routing→fulfillment window. |
| Concurrent orders for shared components contend at routing, not at fulfillment. | Contention surfaces at fulfillment time, after both orders are paid. |
Use it when over-promising during the routing→fulfillment window is a real problem — high order velocity, components shared across multiple BOMs, fulfillment lag that’s measured in days rather than minutes.
Enabling
Section titled “Enabling”The toggle lives at Settings → Inventory → Multi-location as “Track BOM materials as Shopify-reserved between routing and fulfillment.”
- Turn on multi-location sensitive adjustments first. Order reservations require it — the toggle is disabled until multi-location is on.
- Toggle “Track BOM materials as Shopify-reserved…” on.
- New orders from this point forward route through the reservation flow. In-flight orders that were already routed before the toggle keep the legacy deduct-at-fulfillment behaviour.
The webhooks
Section titled “The webhooks”Five Shopify fulfillment-order lifecycle webhooks drive the flow. They are registered automatically on install — no manual setup.
| Webhook | What it triggers |
|---|---|
fulfillment_orders/order_routing_complete | Commit the reservation at the assigned location (or hold it if the fulfillment order arrived in ON_HOLD status). |
fulfillment_orders/placed_on_hold | Release the reservation back to available, then flip its status to held so the next event can re-activate it. |
fulfillment_orders/hold_released | Commit again — re-reserves the same components at the same location. |
fulfillment_orders/moved | Release at the source location and commit at the destination, atomically. If the destination commit fails, the source release is rolled back so no stock is permanently lost. |
fulfillment_orders/cancelled | Release with mode fulfillment_order_cancelled. |
These supplement the existing orders/updated webhook, which still handles order-edit and refund paths.
Lifecycle states
Section titled “Lifecycle states”A reservation row transitions through:
active— committed, holding stock at the routed location.held— was active butplaced_on_holdflipped it; stock has been returned toavailable, awaitinghold_released.consumed/partially_consumed— fulfillment (or a partial fulfillment) consumed the reserved quantities.released— cancelled, refunded, or rerouted; stock returned toavailable.failed— commit attempted but the Shopify push failed for every line. Local state preserved for retry.
Every transition appends an audit row to the reservation log — created, held, hold_released, consumed, released, rerouted_out, rerouted_in, shopify_synced, shopify_sync_failed, shopify_reverted, failed.
How each material kind behaves
Section titled “How each material kind behaves”| Material kind | On commit | On release | On consume |
|---|---|---|---|
| Shopify-linked | Shopify available → reserved via inventoryMoveQuantities at the routed location. | reserved → available at the same location. | reserved decrements; the inventory is permanently consumed. |
| Virtual (Assemblified-only) | Internal available decrements; audit row written. No Shopify call. | available increments; audit row written. | Audit row written; available was already decremented at commit. |
| Pre-assembled draw-down | The BOM’s pre-assembled shelf decrements at the routed location before any raw-material commit happens. | Pre-assembled shelf increments back. | Audit-only; the shelf decrement at commit is final. |
A single reservation row carries lines of all three kinds when the BOM mixes them — the row is the atomic unit, not the line.
Failure handling
Section titled “Failure handling”Shopify mutations can fail (rate-limited, session expired, in-flight network drop). When the Shopify push for a commit fails:
- The local reservation tables still commit — the operator-facing intent is preserved.
- Per-line
shopifySyncStatusflips tofailed. - A “Retry Shopify sync” path replays the failed lines without re-pushing successful ones.
For automated recovery the dedicated retry-shopify-sync endpoint is the right path; re-editing the reservation also fixes it but is the long way around.
Order reservations vs safety-stock reservations
Section titled “Order reservations vs safety-stock reservations”The two features share the word “reservation” but model different things. Don’t confuse them.
| Safety-stock reservations | Order reservations | |
|---|---|---|
| Triggered by | Operator action (“hold N units’ worth of components”) | Shopify fulfillment-order lifecycle (automatic) |
| Owner | A BOM or sub-assembly | An order + fulfillment-order + location |
| Lifespan | Indefinite — operator releases it | Bounded — released or consumed when the FO closes |
| Shopify bucket | safety_stock | reserved |
| Settings page | Per-BOM Reservations tab | Shop-wide toggle |
They can coexist. A BOM can have an active safety-stock reservation holding 10 units’ worth of components and still create order reservations for incoming orders against the same components — the safety stock has already been moved out of available, so the orders just see the remaining available pool.
See Safety-stock reservations for the operator-driven variant.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Single-location stores are a no-op. The dispatcher gates on
multiLocationSensitiveAdjustments; if it’s off, all five webhooks short-circuit. The toggle UI also disables the reservations switch. - Toggle-off doesn’t strand in-flight reservations. Existing rows remain in their current state and continue to consume on fulfillment / release on cancel. New webhooks just stop creating new reservations.
holdis not the same asheld. Theheldstatus is the post-release intermediate state during aplaced_on_hold→hold_releasedcycle. The initialholdModeflag is what’s used when routing-complete arrives already inON_HOLD.- Reroute is atomic. A
fulfillment_orders/movedevent releases at the source and commits at the destination as a unit. If the destination commit fails, the source release is rolled back; you never end up with two active reservations for the same scope. - Late-arrival guard. If a fulfillment webhook arrives before its corresponding routing-complete (rare but possible under retries), the consume path no-ops with
skipped: no_active_reservationrather than over-decrementing. The legacy deduct-at-fulfillment path then handles the order normally. - Inactive BOMs are skipped. A BOM with
status: inactivedoes not produce reservation lines; the components stay inavailablefor the duration of that order, same as the legacy path. - The visible storefront drops at routing, not fulfillment. This is the whole point — but worth flagging for merchants used to the legacy timing. Marketing campaigns timed around shipment dates will see the stock disappear earlier than they may expect.
Where to next
Section titled “Where to next”- Safety-stock reservations — the operator-driven, per-BOM variant.
- Execution model — how orders flow through Assemblified.
- Webhooks — the full Shopify webhook surface.
- Multi-location setup — the guide for getting multi-location adjustments running.