Short answer
When the ERP or warehouse confirms a shipment, look up the Shopify order's open fulfillment orders, map the shipped lines and quantities to fulfillment order line items, and call fulfillmentCreate with the tracking company, number, and URL. Handle partial shipments and multiple packages explicitly, make the call idempotent using the ERP shipment ID, and decide deliberately whether Shopify notifies the customer.
- ERP / WMS Shipment confirmed
- Outbox Shipment ID + lines
- Resolve Open fulfillment orders
- fulfillmentCreate Tracking + line items
- Shopify Customer notified
Shipping confirmation closes the loop on an order. When it fails, customers do not get tracking emails, support volume rises, and the order looks unfulfilled in Shopify even though it left the warehouse days ago. This guide covers the Shopify-specific model and an integration design that handles partial and multi-package shipments correctly.
Understand fulfillment orders
Shopify models fulfillment work through fulfillment orders. Shopify documents a fulfillment order as an item or group of items in an order that are to be fulfilled from the same location; an order can have more than one, even at the same location. Each fulfillment order has line items that reference the original order line items and the quantities still to fulfill.
You do not fulfill an order directly. You create a fulfillment against one or more fulfillment orders, specifying which fulfillment order line items and quantities shipped, with tracking information.
Two other documented details matter:
- Each fulfillment order has a status and a request status. Only fulfillment orders in a state that allows fulfillment should be used.
- Each fulfillment order has a delivery method (shipping, pickup, local delivery, and others). Shopify advises iterating over all fulfillment orders to determine the delivery method for each.
Step 1: Capture shipments in the ERP
The ERP or warehouse system should emit a shipment event when a package is confirmed (not when picking starts). The event should include:
- the ERP shipment ID (your idempotency anchor),
- the ERP order and the Shopify order ID from the external reference,
- shipped lines: SKU and quantity,
- carrier, service, tracking number(s), and tracking URL if available,
- ship date and origin warehouse.
Publish these through an outbox table or equivalent so a crash between “shipment saved” and “Shopify notified” cannot lose the event.
Step 2: Resolve fulfillment order line items
Query the order’s fulfillment orders and their line items:
query FulfillmentOrdersForOrder($id: ID!) {
order(id: $id) {
fulfillmentOrders(first: 20) {
nodes {
id
status
assignedLocation { location { id } }
deliveryMethod { methodType }
lineItems(first: 100) {
nodes {
id
remainingQuantity
lineItem { id sku }
}
}
}
}
}
}
Then match shipped lines to fulfillment order line items:
- Filter to fulfillment orders that are open for fulfillment and assigned to the location matching the origin warehouse (via your location mapping).
- For each shipped SKU, find line items with that SKU and
remainingQuantity > 0. - Allocate the shipped quantity across them, never exceeding the remaining quantity.
- If any shipped quantity cannot be allocated, stop and route the shipment to review — it usually means the order was edited, already fulfilled, or the warehouse shipped something unexpected.
Match on the order line item ID when your ERP stored it at export time; it is more precise than matching on SKU when the same SKU appears on several lines.
Step 3: Create the fulfillment
mutation CreateFulfillment($fulfillment: FulfillmentInput!) {
fulfillmentCreate(fulfillment: $fulfillment) {
fulfillment { id status trackingInfo { company number url } }
userErrors { field message }
}
}
{
"fulfillment": {
"notifyCustomer": true,
"trackingInfo": {
"company": "UPS",
"number": "1Z999AA10123456784",
"url": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
},
"lineItemsByFulfillmentOrder": [
{
"fulfillmentOrderId": "gid://shopify/FulfillmentOrder/1046000828",
"fulfillmentOrderLineItems": [
{ "id": "gid://shopify/FulfillmentOrderLineItem/1072503286", "quantity": 1 }
]
}
]
}
}
The documented FulfillmentTrackingInput accepts company, number, numbers, url, and urls. Use a consistent carrier naming convention; Shopify can generate tracking links for carriers it recognizes, and an explicit URL helps for others.
Map ERP carrier codes to carrier names in a configuration table, and always send the tracking URL when the ERP or carrier integration provides it. This avoids broken tracking links for regional carriers.
Step 4: Make it idempotent
A retry after a timeout must not create a second fulfillment. Defenses:
- Store a mapping of ERP shipment ID → Shopify fulfillment ID, and check it before creating.
- Before creating, re-query the fulfillment orders. If the remaining quantities have already dropped by the shipped amounts, the previous attempt likely succeeded; look up the existing fulfillment by tracking number and record it.
- Process shipments for the same order sequentially.
Handling partial and split shipments
- Partial shipment: create a fulfillment for the shipped quantities only. The fulfillment order stays open for the rest.
- Multiple packages at once: either one fulfillment with several tracking numbers, or one fulfillment per package. Per-package fulfillments give customers clearer tracking and line-level detail.
- Backorders: leave quantities unfulfilled. Do not create zero-quantity fulfillments.
- Shipped from a different location than assigned: fulfillment orders are tied to locations. If the warehouse ships from somewhere else, the fulfillment order may need to be moved first. Treat this as an exception path and test it with your specific setup.
Customer notifications
Choose one sender for shipping notifications. If Shopify sends them, set notifyCustomer: true. If your ERP, WMS, or a marketing platform sends them, set it to false. Sending both is a common, avoidable complaint.
Error handling
| Error | Likely cause | Action |
|---|---|---|
| Quantity exceeds remaining | Already fulfilled, order edited, or duplicate event | Re-query; if already fulfilled, record and finish; otherwise review |
| Fulfillment order not in a fulfillable state | On hold, cancelled, or closed | Review; do not force |
| Unknown location | Missing warehouse mapping | Configuration error; alert |
| Throttled or 5xx | Transient | Retry with backoff |
Security considerations
- Fulfillment write access lets the integration mark orders shipped; protect the token and scope it narrowly.
- Validate tracking URLs against an allowlist of carrier domains if they come from external systems, so customers are never sent to an unexpected site.
Testing
- Single-line, single-package shipment.
- Partial shipment followed by the remainder a day later.
- One shipment with two packages and two tracking numbers.
- Same shipment event delivered twice.
- Shipment for an order cancelled in Shopify after export.
- Order whose items are split across two locations.
Common mistakes
- Trying to fulfill the order as a whole instead of fulfillment order line items.
- Matching on SKU when the same SKU appears on multiple lines.
- Creating fulfillments at pick time rather than at shipment confirmation.
- Double customer notifications.
- No idempotency link between ERP shipment and Shopify fulfillment.
Summary
Emit a shipment event from the ERP when packages are confirmed, resolve the Shopify order’s open fulfillment orders, allocate shipped quantities to their line items, and create fulfillments with tracking information. Use the ERP shipment ID to make the operation idempotent, handle partial and multi-package shipments explicitly, and choose a single system to notify customers.
Frequently asked questions
What is a fulfillment order in Shopify?
Shopify describes a fulfillment order as an item or group of items in an order that are to be fulfilled from the same location. An order can have several fulfillment orders, and you create fulfillments against their line items.
How do I send multiple tracking numbers for one shipment?
The FulfillmentTrackingInput type accepts lists of numbers and URLs as well as single values. If packages ship at different times, creating one fulfillment per package is usually clearer for customers.
Will Shopify email the customer when I create a fulfillment?
The fulfillmentCreate input includes a notifyCustomer flag. Decide whether Shopify or your own system sends shipping notifications so customers do not receive duplicates.
Can I update a tracking number after creating the fulfillment?
Shopify provides a mutation for updating tracking information on an existing fulfillment. Check the current API reference for its name and inputs in your pinned version.
Sources and further reading
Platform-specific statements on this page were checked against these sources on . Vendor APIs change; confirm details in the current documentation before you build.
- [1] Fulfillment service apps and fulfillment orders — Shopify
- [2] fulfillmentCreate mutation — Shopify
- [3] FulfillmentOrder object — Shopify
Related reading
- Guides How to Synchronize Shopify Orders with an ERP How to export Shopify orders to an ERP reliably — choosing the trigger, mapping lines, taxes, discounts and payments, handling edits, and avoiding duplicates.
- Guides How to Handle Refunds and Cancellations in an ERP Integration Model ecommerce refunds, cancellations, and returns in an ERP — credit notes, restocking, partial refunds, shipping refunds, and which system initiates each action.
- Guides How to Implement Retries and Idempotency A practical implementation of retries with exponential backoff and jitter, plus idempotency keys, so ecommerce-ERP integrations can retry safely without side effects.
- Platforms Shopify ERP Integration How Shopify connects to an ERP — the GraphQL Admin API, webhooks, inventory states, fulfillment orders, rate limits — plus the sync patterns and pitfalls that matter.
- Concepts What Is Event-Driven Architecture in Integrations? How event-driven architecture applies to ecommerce-ERP integrations — events, producers, consumers, queues, outbox patterns — and the trade-offs it introduces.