A line-item fulfillment model prevents split shipments, moves, cancellations, and retries from closing Shopify orders or triggering customer actions too early.
A customer orders three products. Two ship from one location today; the third moves to another location and leaves next week. If your Shopify partial fulfillment automation stores one status for the whole order, it can send the wrong delivery email, close a support task too early, or mark revenue as operationally complete while one item is still waiting.
A reliable model is smaller and more precise. Keep the order as the commercial container, then track fulfillment at the line-item quantity level. Below is the state record, update sequence, and failure test suite needed to do that without turning a straightforward integration into a warehouse-management project.
Shopify's own data model points in this direction. An order can have multiple fulfillment orders, and each fulfillment order groups line items that are processed together. Shopify explicitly notes that this lets an order be processed in parts. A fulfillment then represents work completed for one or more of those items. See the official Order and Fulfillment documentation.
Three clocks may be running at once: what the customer purchased, what a location has been assigned to handle, and what has actually shipped. A field such as order_fulfillment_status compresses them into a summary. That works for display and filtering. It is too lossy to drive every downstream action.
One failure pattern is to fire business actions on a transition to "partial." The first parcel triggers a shipping message, a CRM stage change, and a support-task closure. When the remaining item moves, is held, or ships later, the automation has no durable record of which quantity caused each action. Retrying the workflow is risky because the order-level status has not changed, even though the underlying fulfillment work has.
The first version does not need a copy of Shopify. It needs a compact operational ledger keyed below the order level. Use stable Shopify IDs, not SKU or product title, because catalog labels can change and the same SKU can appear in edge cases your automation should not have to interpret.
{
"shop_id": "...",
"order_id": "...",
"fulfillment_order_id": "...",
"fulfillment_order_line_item_id": "...",
"order_line_item_id": "...",
"assigned_location_id": "...",
"ordered_quantity": 3,
"fulfilled_quantity": 2,
"cancelled_or_refunded_quantity": 0,
"open_quantity": 1,
"last_observed_at": "2026-08-26T01:15:00Z"
}
This is a projection your system owns, not a replacement for Shopify. The important design choice is that quantities are explicit. Shopify's fulfillment creation inputs identify fulfillment-order line items and the quantity to fulfill. Shopify also documents that a partial fulfillment request can split the original fulfillment order into submitted and remaining groups. Your local record must therefore tolerate a line item's work moving between fulfillment-order IDs while preserving the connection to the original order line.
Keep the source event ID or webhook receipt separately for duplicate detection. Do not make it the primary key of the business record. One webhook delivery describes an observation; the ledger represents current operational truth.
Process each relevant webhook as a signal to reconcile, not as a command to send an email. Persist the receipt, fetch or read the affected fulfillment state, upsert the line-level projection, then evaluate business rules against the newly committed state. Shopify supports fulfillment-order webhooks for domain events, but the event that wakes the workflow and the state that authorizes a side effect are different things.
A practical rule table might look like this:
| Action | Required state | Deduplication key |
|---|---|---|
| Send shipment email | A new fulfillment with tracking exists | fulfillment_id + tracking_number |
| Mark order fully shipped | Open shippable quantity equals zero | order_id + fully_shipped |
| Create exception task | Open quantity remains and its fulfillment order is on hold or rejected | fulfillment_order_id + exception_type |
| Close shipping support task | Every shippable line is complete or removed | order_id + shipping_task_closed |
Write the side-effect key in the same transaction as the decision whenever your database supports it. If the email provider or CRM call fails afterward, mark the effect as failed and retry that effect. Do not rerun the whole order workflow and hope every downstream system deduplicates it for you.
A test with one item and one location misses the hard cases. The useful acceptance suite changes the grouping or quantity after the order exists:
| Scenario | Expected invariant |
|---|---|
| Two of three units ship | Open quantity is one; the order is not fully shipped |
| Remaining unit moves to another location | Assignment changes without creating a second customer shipment notice |
| One line is removed in an order edit | Completion uses the current required quantity, not the original total |
| A fulfillment is cancelled and recreated | Shipped quantity is not counted twice |
| The same webhook is delivered twice | Ledger state and side-effect count remain unchanged |
| Events arrive out of order | A later reconciliation cannot be overwritten by stale observed state |
Reassignment deserves special attention. Shopify's FulfillmentOrder documentation explains that moving only a subset of line items can close an existing fulfillment order and recreate line items in another one. A design keyed only by fulfillment_order_id can interpret that as deleted work plus brand-new work. Retain the order-line relationship, compare quantities, and make replacement explicit.
For an SMB integration, start with four parts: a webhook receipt table, the line-level fulfillment projection, a side-effect ledger, and a scheduled reconciliation job for recently active orders. Event sourcing, a streaming platform, and a general warehouse orchestration layer can wait until the operating volume or recovery requirements justify them.
The scheduled reconciliation is what keeps the design honest. Webhooks make updates fast; a periodic read repairs missed, delayed, or misinterpreted events. Limit the job to orders with recent activity or open quantity so the recovery path stays inexpensive. Alert only when the source state and local projection still disagree after a retry window.
If your current automation uses one order-level status to trigger customer messages, CRM changes, and operational closure, audit those three actions first. PASMO can help map the fulfillment state, isolate side effects, and build the smallest recovery loop that fits your Shopify and fulfillment stack.