Shopify creates fulfillment orders for you. An order is split into fulfillment orders, each grouping the items expected to ship from one location, and the Shopify Fulfillment Order API is how an app acts on those groups. Whether you can create a fulfillment or only ask for one depends on where the fulfillment order is assigned and which access scopes you hold.
That split catches people out. You fetch an order, find the line items, and go looking for the mutation that creates a fulfillment order to hold them. There isn’t one. The reference for the FulfillmentOrder object states it plainly: “Shopify creates fulfillment orders automatically when an order is created. It is not possible to manually create fulfillment orders.”
What does a fulfillment order actually represent?
Work that is meant to happen. The object models “either an item or a group of items in an Order that are expected to be fulfilled from the same location”, and there can be more than one of them at the same location. A Fulfillment is the other half of the pair, created “to represent the ongoing or completed work of fulfillment”.
Two status fields matter. status moves through OPEN, SCHEDULED, IN_PROGRESS, ON_HOLD and CLOSED. requestStatus tracks the conversation with a fulfillment service, through values including SUBMITTED, ACCEPTED, CANCELLATION_REQUESTED and CANCELLATION_REJECTED.
Do not infer what you are allowed to do from those two fields. Read supportedActions instead, which Shopify populates per fulfillment order:
query OrderFulfillmentOrders($id: ID!) {
order(id: $id) {
fulfillmentOrders(first: 10) {
nodes {
id
status
requestStatus
assignedLocation { location { id name } }
supportedActions { action externalUrl }
lineItems(first: 50) {
nodes { id remainingQuantity lineItem { name sku } }
}
}
}
}
}
The FulfillmentOrderAction enum names the mutation behind each value. CREATE_FULFILLMENT means you can ship it yourself, REQUEST_FULFILLMENT means you have to ask someone else, and the rest cover HOLD, RELEASE_HOLD, MOVE, SPLIT, MERGE, MARK_AS_OPEN, CANCEL_FULFILLMENT_ORDER, REQUEST_CANCELLATION, REPORT_PROGRESS and EXTERNAL. Branch on that array and you never have to guess which of those cases a given order is in.
The docs contradict themselves here, so follow the schema. As of API version 2026-07 the enum entry for CREATE_FULFILLMENT still says “The corresponding mutation for this action is fulfillmentCreateV2”, while the schema marks fulfillmentCreateV2 deprecated with the message “Use fulfillmentCreate instead”.
Which fulfillment orders can your app see?
Scopes decide, and they decide silently. Per the API reference, “An API client will only receive a subset of the fulfillment orders which belong to an order if they don’t have the necessary access scopes to view all of the fulfillment orders.” That is filtering, not failure. Your app gets a shorter list than the merchant sees in the admin, and nothing marks the gap.
The docs describe two usual shapes. A fulfillment service app holds write_assigned_fulfillment_orders and normally not the other two, so “the app will only have access to the fulfillment orders assigned to their location”, or locations, if it registers several services on one shop. An order management app holds write_merchant_managed_fulfillment_orders and write_third_party_fulfillment_orders, which “will allow them to manage all fulfillment orders on behalf of a merchant”. An app that does both jobs should request all of them.
There is a harder limit underneath the scopes. Shopify’s guide for order management apps says: “As of API version 2024-10, you can only create fulfillments for orders that are assigned to a merchant-managed location or for orders that are assigned to a third-party fulfillment service that you own.” Holding write_third_party_fulfillment_orders does not let you ship from someone else’s warehouse. It lets you ask them to.
How do you fulfil an order with the Fulfillment Order API?
If CREATE_FULFILLMENT is in supportedActions, call fulfillmentCreate. It takes fulfillment orders, not line items on the order:
mutation CreateFulfillment {
fulfillmentCreate(
fulfillment: {
notifyCustomer: true
trackingInfo: { company: "Royal Mail", number: "AB123456789GB" }
lineItemsByFulfillmentOrder: [
{
fulfillmentOrderId: "gid://shopify/FulfillmentOrder/5018595819542"
fulfillmentOrderLineItems: [
{ id: "gid://shopify/FulfillmentOrderLineItem/10926793228310", quantity: 2 }
]
}
]
}
) {
fulfillment { id status trackingInfo { company number url } }
userErrors { field message }
}
}
lineItemsByFulfillmentOrder is an array because one fulfillment can cover several fulfillment orders, with one condition from the reference: they must be “associated with the same Order and are assigned to the same Location”. Omit fulfillmentOrderLineItems and the mutation fulfils everything remaining. Include it with quantities to ship a partial box.
The IDs in that array are FulfillmentOrderLineItem IDs, not the LineItem IDs you would use elsewhere in the Admin API. They are different objects with different GID prefixes, and the mutation takes only the first kind.
Tracking that arrives later goes through fulfillmentTrackingInfoUpdate against the created Fulfillment ID, so you can ship first and attach the tracking number once the carrier hands it over.
What happens when the location belongs to a 3PL?
You submit a request and wait. fulfillmentOrderSubmitFulfillmentRequest needs write_third_party_fulfillment_orders:
mutation RequestFulfillment {
fulfillmentOrderSubmitFulfillmentRequest(
id: "gid://shopify/FulfillmentOrder/1046000782"
message: "Gift wrap this one."
) {
submittedFulfillmentOrder { id requestStatus }
unsubmittedFulfillmentOrder { id requestStatus }
userErrors { field message }
}
}
The payload carries three fulfillment order fields for a reason. Pass fulfillmentOrderLineItems to request part of an order and, in Shopify’s words, “Shopify splits the original fulfillment order into two: one with the submitted items and another with the remaining unsubmitted items”. Your stored ID may now cover fewer items than it did before the call, so treat submittedFulfillmentOrder and unsubmittedFulfillmentOrder as the current truth and write both back.
How does a fulfillment service accept the work?
From the other side of the same request, which Shopify documents in build for fulfillment services. A service app registers with fulfillmentServiceCreate, which needs write_fulfillments and creates a location on the shop named after the service. The callbackUrl argument is optional as of API version 2026-01 and fulfillmentOrdersOptIn is deprecated on that input. Skip the callback URL and, per the reference, you submit inventory levels and tracking numbers through the API yourself instead of having Shopify fetch them from your endpoint.
Poll or subscribe, then read the queue:
query IncomingWork {
assignedFulfillmentOrders(first: 25, assignmentStatus: FULFILLMENT_REQUESTED) {
nodes {
id
status
requestStatus
destination { address1 city zip countryCode }
merchantRequests(first: 5, kind: FULFILLMENT_REQUEST) {
nodes { message requestOptions }
}
lineItems(first: 50) { nodes { id remainingQuantity sku } }
}
}
}
assignmentStatus filters to FULFILLMENT_REQUESTED, FULFILLMENT_ACCEPTED, FULFILLMENT_UNSUBMITTED or CANCELLATION_REQUESTED. Leave it out and you get every assigned fulfillment order except those with CLOSED status.
Answering is one mutation, and it requires write_assigned_fulfillment_orders rather than the third-party scope the merchant’s app used to ask:
mutation AcceptRequest {
fulfillmentOrderAcceptFulfillmentRequest(
id: "gid://shopify/FulfillmentOrder/1046000802"
message: "Picking today, shipping tomorrow."
estimatedShippedAt: "2026-09-16T09:00:00Z"
) {
fulfillmentOrder { id status requestStatus }
userErrors { field message }
}
}
fulfillmentOrderRejectFulfillmentRequest is the other half. Accepted work moves under assignmentStatus: FULFILLMENT_ACCEPTED, and the shipping itself still happens through fulfillmentCreate, which you are allowed to call here because the location is yours.
The webhook to subscribe to is fulfillment_orders/fulfillment_request_submitted, and its payload carries original_fulfillment_order, submitted_fulfillment_order and fulfillment_order_merchant_request with the merchant’s message. Verify it like any other Shopify webhook before you act on it, which our guide to verifying Shopify webhook HMAC signatures covers in full.
What we would do
Read supportedActions on every fulfillment order and branch on it. Request the narrowest scope set that matches the job your app actually does, because asking for all of them makes the install screen look like a data grab and buys you nothing if you only ship from your own warehouse. Store fulfillment order IDs as a cache you can refill, never as a foreign key, because one partial request splits a fulfillment order into two.
Importing history is a different job, and the fulfillment order flow is the wrong tool for it. Backfilled orders carry their own fulfilment state, which our write-up on importing historical orders with orderCreate goes through.
Whoooop builds Shopify apps and integrations for UK merchants, including warehouse and 3PL connections that have to keep Shopify and a fulfilment system agreeing with each other. If that is the problem in front of you, our Shopify development work is where to start.