Shopify's documentation specifies that fulfillment orders represent the work intended to be done for an order and are created automatically when an order is placed, rather than manually by merchants or apps [1]. Every fulfillment order binds a set of line items to an assigned fulfillment location [1]. When an order must be fulfilled from multiple facilities, or when items must ship separately, automated systems interact with two core primitives: fulfillmentOrderSplit to isolate unfulfilled quantities into a distinct fulfillment order [3], and fulfillmentOrderMove to redirect fulfillment orders to alternate locations stocking inventory [4]. Because moving orders fails once fulfillment services accept work or when items are closed [4], routing automation must evaluate splits and location assignments before releasing orders to warehouse queues.
1. The FulfillmentOrder Object and the Decoupled Fulfillment Model
In earlier iterations of Shopify's architecture, fulfillment actions were executed directly against an order. Shopify deprecated that direct execution pattern and replaced it with the FulfillmentOrder resource [1]. Under this architecture, an order does not represent the physical unit of fulfillment work. Instead, an order contains one or more fulfillment orders, each assigned to a specific inventory location [1].
Shopify's documentation emphasizes that fulfillment orders cannot be created manually by merchants or third-party applications: they are generated automatically by Shopify background processes when an order is created [1]. Each fulfillment order groups line items and quantities destined for delivery from a single assigned location [1].
This decoupling separates financial commitments from logistical execution. When an order is placed, Shopify evaluates routing rules and creates fulfillment orders assigned to locations stocking the products [1]. Fulfillments are subsequently created against specific fulfillment orders, rather than the parent order, ensuring that picking, packing, and shipment tracking remain bound to the correct physical inventory node [1].
2. The Status Lifecycle: Internal Progress and 3PL Coordination
A fulfillment order operates across two parallel status fields: status, which reflects the internal lifecycle of the fulfillment work [2], and requestStatus, which tracks requests exchanged with fulfillment services and third-party logistics providers [7].
| Status value | Lifecycle meaning documented by Shopify | Permitted progression |
|---|---|---|
OPEN | The fulfillment order is ready for fulfillment [2]. | Work can begin; fulfillments can be created [1]. |
IN_PROGRESS | The fulfillment order is being processed [2]. | Picking and packing are underway [1]. |
SCHEDULED | The fulfillment order is deferred and will be ready for fulfillment after the date specified in fulfill_at [2]. | Transitions to OPEN automatically once fulfill_at arrives [1]. |
ON_HOLD | The fulfillment order is on hold; fulfillment cannot be initiated until released [2]. | Requires release hold mutation before work resumes [8]. |
CLOSED | The fulfillment order has been completed and closed [2]. | Terminal state; work completed or line items reassigned [4]. |
CANCELLED | The fulfillment order has been cancelled by the merchant [2]. | Terminal state; no fulfillments can be created [2]. |
INCOMPLETE | The fulfillment order cannot be completed as requested [2]. | Marked when items cannot be fulfilled [2]. |
When an order is assigned to a fulfillment service location rather than a merchant-managed warehouse, the requestStatus field governs coordination [7]. The initial state for newly created fulfillment orders is UNSUBMITTED [7]. Once the merchant submits a request, the status transitions to SUBMITTED, awaiting an ACCEPTED or REJECTED response from the provider [7].
If a merchant needs to cancel after submission, the request moves to CANCELLATION_REQUESTED [7]. The service must respond with CANCELLATION_ACCEPTED or CANCELLATION_REJECTED [7]. As documented by Shopify, once a fulfillment service accepts a fulfillment request, the merchant can no longer cancel the order directly and must submit a formal cancellation request [1].
3. Splitting Orders: How fulfillmentOrderSplit Operates
When an order contains multiple items that must ship separately, an order management system cannot simply generate separate fulfillments without adjusting the underlying fulfillment orders. Shopify provides the fulfillmentOrderSplit mutation to divide fulfillment orders into discrete units [3].
The mutation accepts the fulfillmentOrderSplits argument, which specifies the target fulfillmentOrderId and an array of fulfillmentOrderLineItems with their respective line item IDs and quantities [3]. Executing this mutation carves the specified items out of the existing fulfillment order and assigns them to a newly created fulfillment order [3].
The GraphQL payload returns two key objects inside fulfillmentOrderSplits: fulfillmentOrder, which represents the newly created fulfillment order holding the extracted items, and remainingFulfillmentOrder, which represents the original fulfillment order holding the remaining quantities [3].
Shopify's documentation notes an important fallback mechanism: if the original fulfillment order cannot be split due to its current state, the mutation creates a replacement fulfillment order instead [3]. Calling this mutation requires either the write_merchant_managed_fulfillment_orders or write_third_party_fulfillment_orders access scope, along with the fulfill_and_ship_orders user permission [3].
4. Moving Locations: fulfillmentOrderMove Semantics and Restrictions
When an inventory node lacks stock or an order management system decides to route fulfillment to a warehouse closer to the shipping destination, it calls the fulfillmentOrderMove mutation [4]. This mutation transfers unfulfilled line items to a new location ID [4].
Shopify documents two distinct operational behaviors when moving items [4]:
- Re-assigning all line items: If all line items in the fulfillment order are transferred to the new location, Shopify updates the
assignedLocationfield on the existing fulfillment order [4]. - Re-assigning a subset of line items: If only a subset of line items is transferred, or if some items have already been fulfilled, Shopify creates a new fulfillment order at the destination location, marks the transferred items as closed on the original fulfillment order, and recreates them in the new record [4].
Shopify enforces strict boundary conditions under which fulfillmentOrderMove will fail [4]:
- The fulfillment order is
CLOSED[4]. - The destination location does not stock the requested inventory item [4].
- The fulfillment order has had manual progress reported, in which case it must first be returned to
OPENbefore moving [4]. - The fulfillment order has a
requestStatusofSUBMITTED,ACCEPTED,CANCELLATION_REQUESTED, orCANCELLATION_REJECTED[4].
This last constraint is critical for order operations. Shopify explicitly blocks location changes while an external fulfillment service is evaluating or processing an order to prevent double-fulfillment race conditions where two distinct facilities ship the same goods [4].
5. Execution Holds: Active Hold Limits and Explicit Release
To prevent fulfillment operations from picking or shipping orders during post-purchase verification, applications apply holds using the fulfillmentOrderHold mutation [5]. Placing a hold transitions the fulfillment order status to ON_HOLD [2].
Shopify documents eight standard hold reasons in the FulfillmentHoldReason enumeration: AWAITING_PAYMENT, AWAITING_RETURN_ITEMS, HIGH_RISK_OF_FRAUD, INCORRECT_ADDRESS, INVENTORY_OUT_OF_STOCK, ONLINE_STORE_POST_PURCHASE_CROSS_SELL, UNKNOWN_DELIVERY_DATE, and OTHER [6].
Each application can place up to 10 active holds per fulfillment order [5]. To maintain multiple concurrent holds, such as a fraud review hold alongside an address verification hold, the application must supply a distinct handle for each hold [5]. If an app attempts to exceed 10 active holds on a single fulfillment order, Shopify returns a user error indicating that the limit has been reached [5].
When releasing holds via fulfillmentOrderReleaseHold, precision is mandatory [8]. The mutation accepts an optional holdIds parameter [8]. Shopify's documentation explicitly warns: if holdIds is not supplied, all holds on the fulfillment order will be released [8]. Releasing all holds indiscriminately causes the fulfillment order to transition back to OPEN prematurely, exposing held items to immediate pick-pack execution before other verification checks have cleared [8].
6. An Operational Routing Method for Multi-Location Stores
The following is SANOCEA's recommended engineering protocol for managing multi-location routing and split shipments against Shopify's Fulfillment Orders API. It translates Shopify's API mechanics into a structured workflow.
Subscribe to the fulfillment orders creation and routing completion webhooks to capture newly assigned fulfillment orders before warehouse dispatch engines poll them.
Query live on-hand quantities at each location before attempting transfers, ensuring the target location stocks the variant to prevent move rejections.
If an order requires fraud scoring, postal address validation, or customer change windows, apply a hold immediately with a unique handle and documented reason code.
Determine whether orders should be split to satisfy delivery cutoffs or consolidated to minimize split shipment freight surcharges prior to sending instructions to warehouses.
When only part of an order needs reassignment, call fulfillmentOrderSplit first to generate clean fulfillment order boundaries, then call fulfillmentOrderMove on the separated record.
Always pass explicit hold IDs to fulfillmentOrderReleaseHold when clearing checks, preventing accidental release of concurrent holds placed by other inspection services.
Log all fulfillmentOrderMove rejections and split shipment frequencies to identify inventory allocation imbalances across regional fulfillment nodes.
7. Technical Constraints to Verify Before Deployment
- Shopify's order routing runs asynchronously via background workers upon order creation; integrations must poll or listen to webhooks rather than assuming immediate static location assignments [1].
- API permissions require specific granular access scopes: an application authorised only for merchant-managed locations will fail to inspect or manipulate fulfillment orders assigned to third-party services [1].
- Fulfillment orders accepted by external 3PLs cannot be moved without an accepted cancellation request [4], requiring upstream middleware to lock routing decisions prior to dispatch submission.
- This article analyzes the documented structure of Shopify's GraphQL Admin API as read on 11 October 2026; merchants should review their specific app scopes and test mutations in sandbox environments prior to production rollout.
Sources
- Shopify GraphQL Admin API: FulfillmentOrder object (retrieved 2026-10-11)
- Shopify GraphQL Admin API: FulfillmentOrderStatus enum (retrieved 2026-10-11)
- Shopify GraphQL Admin API: fulfillmentOrderSplit mutation (retrieved 2026-10-11)
- Shopify GraphQL Admin API: fulfillmentOrderMove mutation (retrieved 2026-10-11)
- Shopify GraphQL Admin API: fulfillmentOrderHold mutation (retrieved 2026-10-11)
- Shopify GraphQL Admin API: FulfillmentHoldReason enum (retrieved 2026-10-11)
- Shopify GraphQL Admin API: FulfillmentOrderRequestStatus enum (retrieved 2026-10-11)
- Shopify GraphQL Admin API: fulfillmentOrderReleaseHold mutation (retrieved 2026-10-11)
