Scale your operation with a tech-enabled 3PL. Get a quote.

Technology / Shopify

Shopify integration for 3PL fulfillment, end to end

Most Shopify integration breakages are not protocol bugs. They are timing, throttling, and idempotency mistakes. This guide covers what a fulfillment integration with Shopify actually has to do, where Shopify Plus changes the math, and where Functions and carrier service callbacks fit in 2026.

2,000
Shopify Plus REST calls per app per minute soft limit[Shopify Dev]
1,000
GraphQL Admin API points per second on Plus[Shopify Dev]
10s
CarrierService callback timeout for shipping rates[Shopify Dev]
48 hr
Webhook retry window before delivery is dropped[Shopify Dev]

TL;DR

  • Shopify and Shopify Plus share the same API surface, but Plus raises rate limits, opens up checkout extensibility, and adds Shopify Functions for custom shipping rates.
  • A clean 3PL integration uses webhooks for events, GraphQL Admin API for sync, and the FulfillmentService API to mark itself as the location-of-record.
  • CarrierService callbacks must respond in roughly 10 seconds or Shopify falls back to cached rates, which is why most teams quote from a precomputed rate table.
  • Shopify Functions replace Shopify Scripts for discount, payment, and delivery customization. They run inside a sandboxed WebAssembly runtime at checkout.

Section 01

Shopify Plus vs standard Shopify, only the parts that matter for fulfillment

Marketing pages frame Plus as a tier upgrade. From a fulfillment integration point of view it is more useful to think of three concrete differences: throughput, checkout customization, and multi-location accounting.[9]

Throughput

On standard Shopify, the GraphQL Admin API gives you about 100 cost points per second per shop. Plus raises that to roughly 1,000 points per second.[8] A single product query with variants and inventory levels can cost 10 to 50 points, so a backfill of 100,000 SKUs against a standard plan takes meaningfully longer than the same job on Plus. Bulk operations can sidestep the per-second budget for backfills.[11]

Checkout customization

Only Plus merchants can deploy Shopify Functions for delivery, payment, and discount logic at checkout, and only Plus has access to checkout UI extensions running in the new web pixel sandbox.[9] If your fulfillment offer involves rules like "weekend cutoff times for ground orders over 5 lb", that logic lives in a Function, not in your 3PL backend.

Multi-location accounting

Both tiers support multi-location inventory. Plus increases the location limit and gives operators more flexibility around split fulfillment. For a 3PL, the practical difference is whether you can register each warehouse as its own location. On standard plans, brands often consolidate to a single "3PL" location, which loses per-DC inventory visibility.[7]

Section 02

Public app, custom app, or direct Admin API

Every 3PL faces the same fork. Build a public app for the Shopify App Store, build a custom app installed by token, or have the merchant generate a private credential and call the Admin API directly. They are not equivalent.

Distribution model trade-offs for 3PL Shopify integrations.

ModelInstall pathAuthBest forWatch out for
Public appApp Store listingOAuthSelf-serve onboarding at scaleListing review, billing API requirements, public support burden
Custom appPer-merchant install linkAdmin API access tokenMid-market 3PLs with managed onboardingToken rotation, scopes locked at install time
Direct API (Plus)Plus-organization custom appAccess token in Plus orgPlus shops with shared brand operatorsOrg-level token blast radius if leaked

For most 3PLs, the right answer is a public app gated to merchants who have signed a fulfillment agreement.[1] The OAuth flow gets you a per-shop offline token, you can request the minimum scopes you need, and Shopify handles webhook signing keys for you.

The OAuth install request a new app needs is small, around eight scopes. Most of the integration risk is what happens after install, not before.

A common pattern with Shopify 3PL apps

The minimal scope set for a fulfillment app is roughly read_orders, write_fulfillments, read_locations, read_products, write_inventory, and the related read_* counterparts. Adding write_shipping gets you the CarrierService API, and write_assigned_fulfillment_orders is required if you want to claim fulfillment orders as a registered fulfillment service.[6]

Section 03

Order, inventory, and shipment sync

The three core flows in any Shopify integration are pulling new orders, keeping inventory honest, and posting tracking back. They look like three jobs. They are actually three event streams that must be reconciled against one source of truth in the WMS.

Orders in

The fastest way to receive an order is the orders/create webhook, which Shopify fires within seconds of checkout. Webhooks must be verified using the HMAC header before the body is trusted.[2] The retry window is 48 hours with exponential backoff, so a temporary outage in your endpoint is recoverable, but a quietly broken HMAC verifier is not.

Treat webhooks as triggers, not as data. The payload may be stale by the time you process it. After verifying the HMAC, fetch the order through the GraphQL Admin API using the order ID and store the canonical version. That gets you the latest financial status, fraud signals, and shipping address edits the customer made post-checkout.

Inventory out

Inventory sync is the integration most teams underbuild. The right model is a per-SKU, per-location truth held in the WMS, pushed into Shopify on a cadence that reflects the actual rate of change. For most brands that means a streaming push on every receive, pick, and adjustment, plus a nightly full reconciliation against the Shopify InventoryLevel records.[7]

Shopify treats inventory levels as connected to a Location, not a Product. If the 3PL is registered as a Location, the integration sets levels through inventoryAdjustQuantities on the InventoryLevel. The common mistake is to push a "set to 50" call when what you actually want is "delta of negative 3", which is the concurrency-safe operation.

Shipments back

Posting tracking back is the operationally noisiest part of a Shopify integration. The right surface is the FulfillmentOrder graph: the 3PL claims the fulfillment order, optionally splits it across packages, and creates a Fulfillment with tracking info that fires Shopify's customer notification.[6] Doing this through the legacy Fulfillments REST endpoint instead of FulfillmentOrders works but loses the per-line-item splits that bundle and pre-order brands rely on.

Section 04

Shopify Functions for delivery and shipping rate logic

Shopify Functions are short WebAssembly programs that run inside Shopify's checkout, replacing the older Scripts. They are the supported way to inject custom delivery rules without an external callback.[3]

For fulfillment, the relevant function targets are cart.delivery_options.transform.run for sorting, hiding, or renaming delivery options, and delivery_customization for things like blocking PO boxes for hazmat lines.

Practical limits

Functions run inside a sandbox with a strict memory and execution budget. They do not have network access. Any data the Function needs at checkout has to be staked at deploy time as metafields the Function can read. That means shipping rate tables baked into a function are practical only when they are short and rarely change. For dense rate cards, the CarrierService API is the better surface.[3]

Section 05

Custom carrier service callbacks for live rates

The CarrierService Admin API lets a third party register a callback URL. When a customer reaches checkout, Shopify POSTs the cart's items, addresses, and currency to that URL and expects a JSON list of shipping rates back.[4]

Latency budget

The published timeout is roughly 10 seconds. In practice, anything over a few hundred milliseconds is a problem because Shopify caches successful responses for repeat carts, but failures fall through to no rates at all.[4] Real-world callback budgets used by experienced 3PLs:

  1. P50
    Under 250 ms

    Rate the cart against an in-memory rate table, skip the TMS round trip on cache hit.

  2. P95
    Under 800 ms

    TMS query for live carrier negotiated rates, with at most one external network hop.

  3. P99
    Under 2 s

    Cold-start tolerance, including a fall-through to a precomputed flat rate table if the live system fails.

The cache trap

Shopify caches the response of a callback against the input fingerprint. If your rates depend on warehouse cutoff time and you do not include that in the cart context, customers get yesterday's quote. The fix is to include any dynamic axis in the response identifier, or to invalidate cache by changing the carrier service registration.

Section 06

What "real-time" actually looks like

"Real-time" in vendor copy is rarely real-time. The relevant question for a brand operator is the lag between an event happening in the WMS and that event being visible in Shopify Admin or in a customer email.

Practical end-to-end lags for a well-engineered Shopify 3PL integration.

EventTypical lagBottleneck
Order created in Shopify, work order created in WMS1 to 5 secondsWebhook delivery and HMAC verification
Pick complete in WMS, label printedUnder 1 secondLocal TMS or carrier API
Tracking back to Shopify, customer notification fired5 to 60 secondsBatched fulfillment posts, webhook fan-out
Inventory adjusted in WMS, level updated in Shopify1 to 30 secondsThrottle budget on InventoryLevel mutations
Inventory adjusted in Shopify, reflected in WMS10 to 60 secondsinventory_levels/update webhook plus internal reconcile

Most "Shopify is slow" complaints from operators are actually integration throttling. Shopify enforces per-shop rate limits using a leaky-bucket model.[5] If your integration is sharing a token budget with a poorly written third-party app, your sync slows down even though Shopify itself is fine. The fix is to monitor the response headers, back off when the budget is low, and consolidate writes through bulk mutations where possible.

If a Shopify integration looks slow, look at the rate-limit headers first. Two thirds of the time the answer is in the bucket, not in the code.

Reconciliation, not just events

Webhooks miss. They get retried, then dropped after 48 hours.[2] A production-grade integration runs a reconciliation loop. The simple version is a nightly diff: pull every order created in the last 72 hours, compare to the WMS, and replay any missed orders. The richer version pulls every fulfillment order in an unfulfilled state and confirms the WMS knows about it. Both versions catch outages your alerting did not.

Section 07

Webhook engineering, the unglamorous parts that decide reliability

Webhooks look simple in a tutorial. In production they expose every weak point in your HTTP stack. Shopify documents the delivery semantics clearly, and the integrations that suffer most are the ones that ignored them.[13]

Verification before parsing

The HMAC signature on every Shopify webhook is computed against the raw request body. If your framework parses the body before the signature check runs, the bytes you sign are not the bytes Shopify signed, and verification fails on perfectly valid payloads. The reliable pattern is to capture the raw body buffer at the very edge of the HTTP stack, verify the signature against that buffer, and only then deserialize.[2]

The other common verification trap is character encoding. Shopify sends UTF-8. Some serverless wrappers (older API Gateway versions, some edge runtimes) coerce request bodies to base64 before handing them off. The HMAC has to be computed against the decoded UTF-8 bytes, not the base64 string. Logging the first sixteen bytes of the buffer you are signing, and the first sixteen bytes Shopify expects, is the fastest way to debug a stubborn 401.

Acknowledge fast, process later

Shopify expects a 2xx response within a few seconds. If your processor takes longer (a database write, a downstream call to a TMS, a slow OAuth refresh), return 200 immediately, queue the work, and process asynchronously. The shape we run is a thin webhook receiver that does HMAC verification, writes the raw payload to a durable queue, and returns 200. A separate consumer fleet drains the queue. This decouples ingest reliability from processing capacity, which matters during traffic spikes when both sides slow down at the same time.

Topic selection

Subscribing to too many topics is as bad as subscribing to too few. The minimum useful set for fulfillment is orders/create, orders/updated, orders/cancelled, fulfillment_orders/order_routing_complete, and inventory_levels/update. Subscribing to broader topics like products/update on a large catalog produces enough volume to fill your queue with noise. Most catalog changes do not affect fulfillment, and the ones that do (variant SKU changes, weight changes) can be caught with a nightly diff.

Section 08

Shopify Markets, multi-currency, and tax math

Most North American DTC brands selling internationally end up on Shopify Markets. Markets adds market-specific pricing, currency, language, and tax treatment as a layer on top of the core store. The integration question is what changes for the 3PL.[12]

Currency and totals

An order created from a non-default market arrives with a presentment currency (what the buyer paid in) and a shop currency (what the merchant accounts in). The 3PL cares about the presentment currency for customs declarations, packing slips, and customer-facing emails, and the shop currency for invoicing the brand. The integration stores both and never recomputes one from the other; rounding rules differ per market and any recomputation can drift cents.

Tax-inclusive pricing is one of the bigger surprises. EU and UK markets return totals with VAT included; US and Canadian markets return totals without sales tax. A 3PL whose customs documentation logic assumes VAT is always added at checkout will under-declare value on EU outbound shipments, which becomes a customs problem at the destination port.

Duties at checkout

For brands selling cross-border, Shopify can collect duties at checkout using DDP (delivered duty paid) terms. When that happens, the order arrives with a duty line and a Shopify-issued declaration ID. The 3PL has to make sure the carrier label is generated with DDP service, not DDU, otherwise the customer pays at the door and Shopify has to issue a duty refund. We write the declaration ID into the carrier label as a reference field so the brokerage matches our shipment to Shopify's record automatically.

Inventory by market

Markets does not split inventory by default. The same Shopify Locations serve every market unless the brand explicitly excludes a location from a market. For a 3PL with US and EU warehouses, the integration registers each warehouse as a Location and the brand decides per market which Locations are eligible. This is a brand-level decision, not a connector-level one, and it determines whether a UK customer ever sees stock at the US warehouse.

Section 09

The Shopify failure-mode catalog we have built up over 200+ integrations

The failures below are not exotic. They are what trips up most teams within the first six months of a Shopify connector running in production. The list is short on purpose because the same handful of mistakes account for most of the support tickets.

1. Variant ID drift

Shopify variant IDs are stable, but variant SKUs are not. A brand admin can edit an SKU at any time. If the WMS keys off the SKU instead of the variant ID, an SKU rename silently breaks the link between Shopify variants and WMS items. The fix is to key off the GraphQL global ID and treat SKU as a human-readable label. Reconcilers flag any SKU rename for review.

2. Multi-pack listings without bundle definitions

A common pattern: the brand creates a Shopify product called "Three-pack" with its own SKU, and assumes the 3PL knows that means three units of the single-pack SKU. Without a bundle contract, the WMS picks one unit of "Three-pack" and ships an empty box. We require either a bundle app's metafield contract or an explicit bundle definition uploaded before going live, and our reconciler refuses to release any new product as fulfillable until that contract exists.

3. Default location not migrated

Shopify creates a default Location ("Online Store" or the merchant's business address) on store setup. Inventory there is not fulfillable from the 3PL warehouse unless the brand explicitly transfers it. New brands cutting over from in-house fulfillment routinely forget this step, the integration shows zero stock at our warehouse for every SKU, and orders route to the wrong place. The fix is documented in our runbook as the first item to verify on day-one cutover.

4. App-managed inventory

Shopify lets a single fulfillment service register itself as the manager of an inventory item. If a previous app already claimed the item, our integration cannot write to it until the brand removes the prior registration. This is the most common cause of "we pushed inventory but Shopify did not move" support tickets. The fix lives in the Shopify admin, not in the API, and we surface a list of conflicting claims during onboarding.[7]

5. Cancellations after pick

A customer cancellation that arrives in Shopify after the WMS has already released the work order to the floor is a recoverable case but not a clean one. The right pattern is to attempt to intercept the work order; if the pack is already complete, ship anyway and process as a return. The wrong pattern is to try to reverse the pick, which usually leaves both systems out of sync. Our default is intercept-or-ship, with a configurable cutoff window per brand.

Most of the Shopify support tickets a 3PL closes in week one trace to a missed scope, an unmigrated default location, or an unfulfillment-aware bundle app. The protocol works. The fit-up is what bites.

Embedded admin and observability

For ongoing operations, an embedded Shopify admin surface (built with App Bridge) is the right place to expose the integration's state to the brand.[14] Webhook delivery success rate, rate-limit headroom, reconciler delta counts, and a per-order audit log are the four tiles that close the most support tickets without a phone call. Building that surface up front is cheaper than answering "did you receive my order" tickets one by one.

The audit log is also where most production debugging happens. When a brand opens a ticket about an order that did not ship, the engineer's first question is which webhook arrived, when, what was in the payload, and what the reconciler decided. A searchable per-order timeline that joins the webhook ingest log, the GraphQL fetch result, the WMS work-order events, and the carrier label receipt closes those tickets in minutes instead of hours. We retain ninety days of these logs hot and the prior twelve months in cold storage, which has been long enough for every dispute we have seen.

Talk to us

Ship Shopify orders without the integration tax

Warpspeed is a Shopify-aware 3PL. We register as a fulfillment service per location, sync inventory through the GraphQL Admin API, and post tracking through the FulfillmentOrder graph so customer notifications fire correctly.

Sources

  1. [src-1]Shopify Admin API referenceShopify Dev
  2. [src-2]Webhooks overviewShopify Dev
  3. [src-3]Shopify FunctionsShopify Dev
  4. [src-4]CarrierService APIShopify Dev
  5. [src-5]API rate limitsShopify Dev
  6. [src-6]FulfillmentService APIShopify Dev
  7. [src-7]Inventory states referenceShopify Dev
  8. [src-8]GraphQL Admin API rate limitsShopify Dev
  9. [src-9]Shopify Plus featuresShopify
  10. [src-10]Order webhook payload referenceShopify Dev
  11. [src-11]GraphQL bulk operationsShopify Dev
  12. [src-12]Shopify Markets referenceShopify
  13. [src-13]Webhook delivery and retry policyShopify Dev
  14. [src-14]App Bridge and embedded admin patternShopify Dev

Related