Integrations / Shopify
Shopify integration setup, sync rules, and the pitfalls
This is the implementation companion to our Shopify integration deep dive. It covers the install steps, the sync rules we apply by default, and the specific failure modes that catch new brands by surprise.
TL;DR
- We install as a Shopify app with the minimum scopes needed for orders, inventory, and fulfillment.
- Each Warpspeed warehouse is registered as a Shopify Location and a fulfillment service so customer notifications fire correctly.
- We post tracking through the FulfillmentOrder graph, not the legacy fulfillments REST endpoint, to keep per-line splits intact.
- Reconciliation runs hourly for inventory and nightly for orders. If a webhook is missed, the reconciler catches it.
Section 01
The install path
We use a public Shopify app distributed through a private install link to brands under contract. The OAuth flow returns an offline access token, which we store encrypted and rotate on demand.[1]
- Step 1Click install
Brand admin visits a per-merchant install link, signs in to Shopify, and approves the requested scopes.
- Step 2Scope review
Shopify presents the scope list. The default for Warpspeed is read_orders, write_fulfillments, read_locations, write_inventory, read_products, read_inventory, write_assigned_fulfillment_orders, and read_fulfillments.
- Step 3Webhook subscriptions
On install we subscribe to orders/create, orders/updated, orders/cancelled, fulfillment_orders/order_routing_complete, and inventory_levels/update.
- Step 4Location registration
We create or claim a Shopify Location per Warpspeed warehouse and register it as a fulfillment service. Without this step, customer notifications still come from Shopify but tracking attribution gets messy.
- Step 5Catalog seed
We pull the full product and variant catalog using GraphQL bulk operations. This is also when we surface missing weights and dimensions for the brand to backfill.
Section 02
Sync rules we apply by default
The default sync direction and cadence per object class.
| Object | Direction | Trigger | Frequency |
|---|---|---|---|
| Order | Shopify to WMS | orders/create webhook | Within seconds |
| Order edits | Shopify to WMS | orders/updated webhook | Within seconds, locked once shipped |
| Cancellation | Shopify to WMS | orders/cancelled webhook | Immediate; rejected if already picked |
| Fulfillment | WMS to Shopify | Pack confirmed in WMS | Within seconds via FulfillmentOrder |
| Inventory delta | WMS to Shopify | Receive, pick, adjust, count | Streaming; reconciler catches misses |
| Reconciliation pull | Shopify to WMS | Cron | Hourly inventory, nightly orders |
Order edits and the locked window
Shopify lets the brand edit an order after it is created: addresses, quantities, notes. We accept those edits up until the WMS marks the work order as picked.[3] After that, edits queue up and get applied to the post-shipment fulfillment record only if they are compatible (for example, customer note is fine, line removal is not). Operators who do not understand this lock window get surprised when a "quick edit" does not propagate.
Inventory writes are deltas, not sets
We use inventoryAdjustQuantities with a delta input. A "set to 73" instruction would race against any other admin or app updating the same level.[4] Deltas are concurrency-safe because Shopify resolves them server-side using the current authoritative count.
Section 03
The five pitfalls that catch most brands
1. Webhook silence
Shopify retries webhooks for 48 hours.[5] If your endpoint returns a 5xx for two days straight, those notifications are gone. The fix is the reconciler, which pulls every order from the prior 72 hours every night and replays anything missing.
2. The "Online Store" location
Shopify creates a default location on store setup. Inventory there is not fulfillable from a 3PL warehouse unless the brand explicitly moves it.[6] A common cutover mistake is missing this step, which makes Shopify route orders to the wrong location and not surface them to the 3PL at all.
3. Variant SKU collisions
Shopify allows two variants to share an SKU. Most WMSes do not. The fix is to enforce uniqueness at install and surface duplicates as a blocker. Brands often have legacy "DUMMY" or "PARENT" SKUs from old import scripts that need cleanup.
4. Bundles without a bundle product
Many bundle apps do not surface child SKUs in the order line items, only the parent bundle SKU. Without a bundle definition synced to the WMS, the warehouse picks the parent and ships an empty box. We require either the bundle app's metafield contract or an explicit bundle definition uploaded before going live.
5. Markets and currency math
Shopify Markets adds market-specific pricing and tax rounding. Order totals from a Markets-enabled store can have rounded cents that do not equal the line item sum.[8] The integration treats Shopify's order total as the source of truth and never recomputes from line items.
“Most of the integration support tickets we close in week one are about a missing scope, a missed location migration, or a bundle app the brand forgot to mention.”
Section 04
The checks we run quietly
The checks below run on every Shopify integration we operate. They are boring on purpose. The goal is to find the problem before the brand opens a ticket.
- Webhook health: per-topic delivery success rate over the last hour, paged if any topic drops below 99 percent.
- Rate-limit headroom: response headers from the Admin API parsed and graphed.[7] Alert at 80 percent budget usage so we back off before we get throttled.
- Reconciler delta: orders found by the reconciler that were not previously in the WMS. A non-zero count is normal; a sudden spike is investigated.
- Inventory delta: SKUs where Shopify and the WMS disagree by more than a tolerance. Tolerances are SKU-specific because cycle counts have lag.
- Stranded fulfillment orders: any FulfillmentOrder in unfulfilled state older than the brand's promised SLA. Pages on-call.
Section 05
Why we use the FulfillmentOrder graph, not legacy fulfillments
Shopify offers two ways to mark an order as shipped. The legacy REST fulfillment endpoint is older, simpler, and still works. The FulfillmentOrder graph is newer, more granular, and the path Shopify recommends.[9] We default to the graph because it handles every modern fulfillment pattern correctly.
Per-line and per-package fulfillment
A real order often ships in pieces. A bundle may pick from two warehouses. A pre-order may ship later than an in-stock item on the same order. A backorder may resolve weeks after the original. The legacy endpoint squashes these into a single fulfillment record per order; the graph represents each as a discrete FulfillmentOrder. That distinction is what makes correct customer notifications possible. Without it, the second shipment on a multi-package order does not trigger a customer email.
Claiming the order
When a fulfillment service is registered against a Location, Shopify routes new fulfillment orders to that service automatically. The integration calls fulfillmentOrderAcceptFulfillmentRequest when it is ready to ship and fulfillmentCreate when the package is in carrier hands. The state machine is explicit, which is what makes auditing what happened to a specific order tractable.
Section 06
B2B, wholesale draft orders, and Plus B2B Edition
Most Shopify integrations focus on DTC. The other side of the store is B2B, which has its own order shape, its own SLAs, and its own integration touchpoints.[11]
Draft orders and net terms
On standard Shopify, B2B is usually shaped as draft orders the brand creates manually. The 3PL receives them through the same orders/create webhook once the draft is finalized and paid. Net-terms invoicing happens outside Shopify, so the integration does not need to model it; we treat the order as paid for fulfillment purposes once it has the paid status, even if the actual cash is collected weeks later.
Plus B2B Edition
Plus B2B Edition adds first-class company accounts, customer-group pricing, and quote-to-order flow inside Shopify. Orders from B2B Edition arrive through the same webhook surface, with additional metafields for company and quote context. We map company-level identifiers to the WMS customer hierarchy so packing slips and BOLs can route correctly to the company's receiving dock, not to the individual buyer.
Operational differences
B2B orders typically pick larger units, ship to commercial addresses with receiving hours, and may need BOL paperwork or palletized shipment. The integration tags B2B orders for a different pick path inside the warehouse, which often means a different cutoff (LTL pickup happens earlier than parcel) and a different pack station. None of this is invisible; we surface the per-channel cutoff and pack pattern in the brand admin so the brand can set buyer expectations correctly.
Section 07
What the brand sees, embedded in Shopify admin
The integration is not just a data pipe. It is also a UI inside the brand's own admin. We use App Bridge to render an embedded surface that looks native to Shopify.[10] The brand never has to tab over to a separate console for routine operations.
The four tiles that close most tickets
The embedded admin's home view is four tiles. Order lag (P50 and P95 order-to-pick time over the last 24 hours). Fulfillment lag (pick to tracking-back time). Inventory variance (per-SKU disagreement count between Shopify and the WMS). Reconciler health (events resolved by reconciler in the last hour, with link to detail). When the brand asks "is something wrong", the answer is on this view.
Per-order audit drill-down
Clicking into an order shows the full timeline: webhook ingest timestamp, GraphQL fetch result, work-order creation, pick assignment, pack confirm, label generation, tracking write-back. Every step has a millisecond-level timestamp and a payload preview. When a customer asks "where is my order", the answer is in this view.
Catalog hygiene console
The catalog hygiene tab lists every variant that has missing or invalid fulfillment-relevant data: missing weight, missing dim, missing HS code on an internationally-shipping SKU, duplicate SKU collisions, ambiguous bundle definitions. Each item links to the Shopify admin row that needs fixing. The brand can clear these without our help.
“The integration has not earned its place if a brand has to leave Shopify admin to use it. Tabbing to another tool is friction; we keep the brand inside the admin they already know.”
Section 08
Holding up at peak, what changes during BFCM
Black Friday and Cyber Monday are the test the integration was built for. Most brands' annual order volume is concentrated in a few weeks; the integration that runs cleanly in March can still fall over in late November because the failure modes that emerge at 10x volume are different from the ones that emerge at 1x.
What we lock down before peak
Two weeks before BFCM we freeze non-critical changes to the integration codebase. New features ship after the New Year. We pre-warm the database connection pool, expand the worker fleet, and run a load test that pushes an order volume equal to the prior year's peak through a staging shop. Anything that fails in staging gets fixed before the actual surge.
The rate-limit math during peak
Shopify's rate-limit budgets are per-shop, not per-app. When a brand has six apps installed and three of them are doing inventory writes, peak traffic exhausts the budget faster. We do not control what other apps do, but we do control how aggressively we consume the budget. During peak, we throttle our own non-critical writes (catalog reconciler, weight backfills, audit-log mirroring) so the budget stays available for orders and fulfillments.[7]
Synthetic order monitoring
During peak we run a synthetic order through the integration every five minutes. The synthetic flows the entire path: webhook ingest, work-order creation, simulated pack confirm, fulfillment write-back. Any latency above the SLO threshold pages the on-call. The synthetic catches problems that real order traffic does not because it covers paths that real orders may not exercise during a particular hour.
Section 09
Migrating off another 3PL onto our Shopify integration
Most Shopify brands switching to us are switching from another 3PL. The migration is faster than a green-field implementation because the catalog, the SKUs, and the shopify-side configuration already exist; the work is the parallel run, the inventory transfer, and the cutover.
The handover from the prior 3PL
The prior 3PL has to disable its Shopify app at a coordinated cutover time, which usually happens after a final batch ships from the old warehouse. We schedule cutover for a Tuesday or Wednesday morning to leave maximum runway for unforeseen issues before the weekend. Both apps can briefly co-exist during the parallel run window, but only one can hold the fulfillment-service registration on a given Location at a time.
Inventory transfer
Inventory moves on its own schedule, usually freight from the prior 3PL to ours. As units arrive, we receive them into the new Location and the integration pushes the new counts to Shopify. Shopify stops routing orders to the old Location once it shows zero stock; brands sometimes prefer to flip the routing earlier and accept short-window backorders for the in-transit inventory. We support both patterns.
Open orders at cutover
Open orders sitting in the prior 3PL's queue need a decision. The clean answer is the prior 3PL ships them and the new 3PL takes responsibility for any new orders from the cutover timestamp forward. The unclean answer, which sometimes happens when the prior relationship is ending badly, is that the new 3PL inherits the open queue. Either way, the integration has to know which orders are "new" so it does not double-fulfill.
Common migration surprises
Three surprises come up reliably. First, a Shopify app the brand forgot about is still writing inventory; we surface this on day one and the brand uninstalls or scopes it. Second, the prior 3PL's app held a fulfillment-service registration that needs to be released before our app can take over; this is a five-minute fix in admin once identified, but it blocks fulfillment writes until done. Third, the legacy integration was using the REST fulfillments endpoint, leaving multi-package orders partially-fulfilled in the graph; we run a one-time cleanup to migrate those to the FulfillmentOrder model so future writes behave correctly.
The fourth surprise that sometimes appears is shipping-zone differences between the old and new warehouses. A brand whose buyers concentrated on the West Coast under the prior 3PL may see different transit-time distributions when our warehouse is in the Midwest. We surface a before-and-after zone-mix preview during onboarding so the brand can decide whether to adjust their shipping promise copy on the storefront.
Talk to us
We can be live on Shopify in days, not weeks
The mechanical part of a Shopify cutover is fast. The slow part is catalog cleanup. We will tell you what to fix before we set a go-live date.
Sources
- [src-1]Shopify app installation overview— Shopify Dev
- [src-2]OAuth scopes reference— Shopify Dev
- [src-3]FulfillmentService API— Shopify Dev
- [src-4]InventoryLevel mutations— Shopify Dev
- [src-5]Webhook delivery and retries— Shopify Dev
- [src-6]Locations API— Shopify Dev
- [src-7]API rate limits— Shopify Dev
- [src-8]Shopify Markets— Shopify
- [src-9]FulfillmentOrder graph reference— Shopify Dev
- [src-10]App Bridge embedded admin pattern— Shopify Dev
- [src-11]Shopify Plus B2B Edition— Shopify