Skip to content

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 availablereserved 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.

  • 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

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 materialsavailable drops, reserved rises by the same amount, via inventoryMoveQuantities against Shopify’s native buckets.
  • Virtual materials — Assemblified’s internal available drops; 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 consumedreserved 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.

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 reservationsWithout 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.

The toggle lives at Settings → Inventory → Multi-location as “Track BOM materials as Shopify-reserved between routing and fulfillment.”

  1. Turn on multi-location sensitive adjustments first. Order reservations require it — the toggle is disabled until multi-location is on.
  2. Toggle “Track BOM materials as Shopify-reserved…” on.
  3. 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.

Five Shopify fulfillment-order lifecycle webhooks drive the flow. They are registered automatically on install — no manual setup.

WebhookWhat it triggers
fulfillment_orders/order_routing_completeCommit the reservation at the assigned location (or hold it if the fulfillment order arrived in ON_HOLD status).
fulfillment_orders/placed_on_holdRelease the reservation back to available, then flip its status to held so the next event can re-activate it.
fulfillment_orders/hold_releasedCommit again — re-reserves the same components at the same location.
fulfillment_orders/movedRelease 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/cancelledRelease with mode fulfillment_order_cancelled.

These supplement the existing orders/updated webhook, which still handles order-edit and refund paths.

A reservation row transitions through:

  • active — committed, holding stock at the routed location.
  • held — was active but placed_on_hold flipped it; stock has been returned to available, awaiting hold_released.
  • consumed / partially_consumed — fulfillment (or a partial fulfillment) consumed the reserved quantities.
  • released — cancelled, refunded, or rerouted; stock returned to available.
  • 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.

Material kindOn commitOn releaseOn consume
Shopify-linkedShopify availablereserved via inventoryMoveQuantities at the routed location.reservedavailable 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-downThe 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.

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 shopifySyncStatus flips to failed.
  • 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 reservationsOrder reservations
Triggered byOperator action (“hold N units’ worth of components”)Shopify fulfillment-order lifecycle (automatic)
OwnerA BOM or sub-assemblyAn order + fulfillment-order + location
LifespanIndefinite — operator releases itBounded — released or consumed when the FO closes
Shopify bucketsafety_stockreserved
Settings pagePer-BOM Reservations tabShop-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.

  • 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.
  • hold is not the same as held. The held status is the post-release intermediate state during a placed_on_holdhold_released cycle. The initial holdMode flag is what’s used when routing-complete arrives already in ON_HOLD.
  • Reroute is atomic. A fulfillment_orders/moved event 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_reservation rather than over-decrementing. The legacy deduct-at-fulfillment path then handles the order normally.
  • Inactive BOMs are skipped. A BOM with status: inactive does not produce reservation lines; the components stay in available for 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.