When an operator combines two orders inside ShipStation, SS keeps the merged order in its UI but drops it from the public API — the secondary /orders?orderNumber=X query returns nothing, the secondary /shipments?orderNumber=X returns nothing, and even fetching by SS orderId returns 404. The WMS sync, which leaned on those endpoints, would silently skip the order and leave it sitting in Awaiting Shipment indefinitely, with its inventory still committed.
This release closes that gap end-to-end.
What the sync now does
For every awaiting_shipment order older than 6 hours, the cron walks a three-step cascade until something resolves it:
-
Lineage — The candidate hunt finds the parent via
advancedOptions.mergedOrSplit=trueandmergedIds[]. When exactly one shipped order in the orphan's customer + ±14d window carries these markers, ShipStation has told us directly that this is the parent. Authoritative — no heuristic involved. -
SKU-subset (gated) — Falls back to matching the orphan's items as a subset of a candidate's full items list, but ONLY accepts candidates with
mergedOrSplit=true. The previous gateless version misattributed orders to coincidental SKU-superset strangers; the gate eliminates that whole class of false positives. -
Address — When the customer-name spelling differs between Shopify ("Stacie Smith") and the ShipStation shipping address ("Stacey Smith") — which happens more than you'd think — the name-based candidate hunt returns nothing useful. The address fallback scans WMS locally for orders sharing the orphan's normalized street + ZIP (USPS abbreviations expanded, so
HwymatchesHighway), then asks SS to confirmmergedOrSplit=truebefore accepting.
Once a parent is identified, the orphan is marked Shipped under the parent's tracking number, its committed_quantity is released, and the merged_into_order_id FK is stamped so the relationship is queryable forever.
What you'll see on the orders themselves
- On the orphan's detail page — an indigo
Combined with # PARENTpill in the header. Clicks straight through to the parent order. - On the parent's detail page — a
Includes # ORPHANpill for each absorbed sibling, and aFrom # ORPHANpill next to each line item the orphan contributed. - On the orders listing — a small chain-link icon next to the order number for any order on either side of a merge, plus a new Combined advanced filter (Any / As orphan / As parent / Not combined).
The parent's local shipment table now mirrors what ShipStation actually dispatched — full combined items list, with each line attributed to whichever order it originated on. If the parent had three lines pre-merge and the orphan added a fourth, you'll see all four rows locally, the fourth tagged with the orphan's order number.
Auto-cancel for orders that never resolve
Some orders never show up in ShipStation at all — typically because they were cancelled in SS outside the sync window, or fulfilled by a third-party logistics provider that doesn't report back to the WMS.
When the resolver returns NOT_FOUND and the order is older than the Auto-cancel stale orders after threshold (defaults to 5 days, set in Settings → Scheduled Tasks → ShipStation Order Sync), the sync cancels it locally — releasing the committed inventory and clearing it from your awaiting-ship dashboards. The cancellation is stamped with cancel_reason_code = stale_orphan and a descriptive reason so the audit trail is unambiguous.
Set the threshold to 0 to disable the auto-cancel behaviour entirely.
What we tested
The resolver ran clean against 150 of the oldest awaiting orders on a recent VaporDNA snapshot:
| Batch | Merge applied | Stale-cancelled | Errors |
|---|---|---|---|
| 25 | 25 (23 lineage / 2 address) | n/a | 0 |
| 100 | 99 (90 lineage / 4 address / 5 subset) | n/a | 0 |
| 25 | 24 (24 lineage) | 1 | 0 |
Every order in 150 landed in a deterministic, audited final state — either shipped-and-linked or cancelled-with-reason. Zero residual awaiting_shipment from the resolution sweep.
Every claimed merge was independently verified against ShipStation's own mergedOrSplit flag + mergedIds[] lineage data — 24/24 in the deep-dive batch.
Operator playbook
- The cron runs every 10 minutes by default; tune the schedule in Settings → Scheduled Tasks → ShipStation Order Sync.
- The cron's per-run limit is the
--limitoption onphp artisan sync:shipstation-orders --sync-statuses --limit=N. Production currently runs with--limit=1000. - To clear a known backlog manually, use
--limit=Nwith whatever size you're comfortable with — the API rate-limit handling will pace the calls automatically. - Combined orders can be triaged from the listing with the new
Combinedfilter; sort by Status to surface remaining stragglers.
Heads-up
For parents that absorbed two or more orphans, the per-row From #X attribution is skipped (we can't disambiguate which orphan contributed which line without dereferencing each merged SS orderId, which 404s). The orphan side still resolves correctly and the parent still shows Includes #X pills for each sibling in the header — only the per-row item badges are omitted in that case.