
Connecting Shopify Plus to NetSuite, SAP or Dynamics 365 is rarely blocked by the API itself. Both ends publish full API references, both speak JSON over HTTPS, and both publish most of their limits. What breaks a Shopify ERP integration is everything the reference docs don’t put in one place: which system owns a given field, what happens when a webhook never lands, and who upgrades the connector when Shopify retires the API version it’s pinned to.
This is written for the operator or technical lead scoping a Shopify ERP integration, or trying to rescue one that’s already syncing bad data. The awkward part of any ecommerce integration with ERP software on the far end is that both systems were built to be the authority, and neither one will tell you when it’s been overruled.
Vendors change connector pricing without notice, so re-pull any price you’re about to put in a business case.
A Shopify ERP Integration Is an Agreement About Which System Is Allowed to Be Wrong
Shopify’s API branches on your answer to this decision rather than assuming one. Shopify splits integrations into two cases (opens in new tab): when Shopify is your source of truth, you use targeted mutations to change specific fields, and when an external system is your source of truth, you use a declarative sync mutation that replaces the complete product state in one operation. Shopify names ERPs and PIMs explicitly in the second case.
On that second path, productSet creates or updates the entire product state, and it replaces all data rather than merging it. Where that bites depends on the field type. productSet (opens in new tab) deletes existing entries that aren’t included in the mutation’s input for list fields such as collections, metafields and variants, while for every other field type it updates only the included fields. So if your merchandising team adds a metafield or a collection in the Shopify admin and the nightly ERP job runs productSet without it, that entry is gone the next morning with no error anywhere.
So the system-of-record decision is per field, not per object. Product title might belong to the ERP while product description, images and SEO fields belong to Shopify, and the only way to hold that line is to make the sync job write a field subset rather than a whole object.
The Inventory State Your Shopify ERP Integration Can't Write
Shopify tracks eight inventory states (opens in new tab): incoming, on hand, available, committed, reserved, damaged, safety stock and quality control. On hand is a derived total. It equals the sum of the quantities in the available, committed, reserved, damaged, safety stock and quality control states. You can write to it, and inventorySetOnHandQuantities exists for exactly that, but you’re setting a total whose split across those six you don’t control.
You can’t use the GraphQL Admin API to adjust or move inventory quantities in the committed state, because Shopify manages committed stock itself through order creation, fulfillment, draft order reservations and transfers marked ready to ship. An ERP that thinks in a single quantity-on-hand figure and pushes it straight to Shopify is overwriting a number that already had open orders subtracted from it.
That mechanism is the ordinary cause of overselling in a Shopify ERP integration, and it doesn’t announce itself. The ERP is correct about the warehouse, Shopify is correct about the open orders, and the sync job is the only thing in the system that’s wrong.
An Error Your Shopify ERP Integration Used to Get, and No Longer Does
Two changes across 2026 turned a loud failure into a quiet one, and they landed in different versions. In 2026-04, deactivating an inventory level stopped clearing its quantities, and the level gained an isActive (opens in new tab) field. Inactive levels can be adjusted just like active ones, so the API now returns these inactive levels.
Then in 2026-10, inventory quantities became adjustable at any location (opens in new tab), so the condition that used to raise ITEM_NOT_STOCKED_AT_LOCATION can no longer occur and the error is no longer emitted. It’s gone from inventoryAdjustQuantities, inventoryMoveQuantities, inventorySetQuantities and inventorySetOnHandQuantities.
A nightly job that pushed stock for an item not stocked at a location used to fail visibly, land in your error log, and get looked at. On 2026-10 the same push succeeds. Nothing tells you the location was never activated for that item, and your alerting has one fewer signal than it had.
The read path is where it compounds. The includeInactive argument (opens in new tab) defaults to false, so existing queries continue to return only active inventory levels. A reconciliation job written before 2026-04 can write a quantity successfully and then not see it when it reads back, and report the location as clean. Pass includeInactive: true on reconciliation queries, check isActive on what comes back, and treat an inactive level holding stock as a work item rather than a row to skip.
Compare and Swap, and Why Shopify Asks You Who's in Charge
The lost-update problem runs like this: you read 100 units, another process sells 20 while you’re deciding, you write 90 based on your stale read, and the true figure should have been 70. The fix is the changeFromQuantity field, which lets you declare the quantity you expect to find before your change applies, and returns a CHANGE_FROM_QUANTITY_STALE error when the current value doesn’t match.
Some inventory mutations implement it, such as inventoryAdjustQuantities, inventoryMoveQuantities, inventorySetQuantities and productVariantsBulkUpdate, so check the reference for whichever mutation your connector actually calls. Shopify introduced it (opens in new tab) effective in API version 2026-01.
Shopify’s guidance is that if you don’t care about the current quantity because you’re the source of truth, you don’t need to implement compare and swap and should opt out. That opt-out is literal. changeFromQuantity is required in 2026-04, with an explicit opt-out, and the requirement is a breaking change, so declining to use it is now something you have to say in the request rather than something you get by omission.
Your answer to the system-of-record question determines which code you write, so you can’t defer it to build time.
While you’re in there, set referenceDocumentUri on inventory adjustments. It lets you trace every inventory adjustment back to its source, and it accepts a GID-style value pointing at the ERP document that caused the change. Skipping it costs nothing on day one and costs an afternoon every time finance asks why a location is off by eleven units.
Every Shopify ERP Integration Runs on an Expiring API Version Pin
Shopify releases a new API version every three months (opens in new tab) at the start of the quarter and supports each stable version for a minimum of 12 months, with at least nine months of overlap between consecutive versions. Release candidates may include backwards-incompatible changes, so they’re not recommended for production. Here’s the version table, with the date each version stops being accessible.
| Version | Release date | Accessible until |
|---|---|---|
| 2025-07 | July 1, 2025 | July 16, 2026 15:00 UTC |
| 2025-10 | October 1, 2025 | October 16, 2026 15:00 UTC |
| 2026-01 | January 1, 2026 | January 16, 2027 15:00 UTC |
| 2026-04 | April 1, 2026 | April 16, 2027 15:00 UTC |
| 2026-07 | July 1, 2026 | July 16, 2027 15:00 UTC |
| 2026-10 | October 1, 2026 | October 16, 2027 15:00 UTC |
| 2027-01 | January 1, 2027 | January 16, 2028 15:00 UTC |
Shopify calls that third column “Accessible until,” not “support ends,” and the distinction is the entire failure mode. If your app targets an inaccessible version, Shopify falls forward and responds using the oldest accessible stable version, so requests to a retired 2026-10 are served as 2027-01. Responses carry an X-Shopify-API-Version header. If it differs from what you requested, your app is targeting an inaccessible version and Shopify has fallen forward to the default.
So a missed uptake on a bespoke connector usually doesn’t produce a clean outage. It produces a connector still addressing a version it was written against and quietly receiving responses built from a newer schema, with removed fields returning nothing and new required arguments unsent. That’s the same failure as an oversold SKU: two systems that both believe they’re right, syncing successfully, and disagreeing.
Microsoft’s Business Central connector shows what that cadence does. The connector is built on the Shopify GraphQL Admin API (opens in new tab), which Shopify versions every three months and, in Microsoft’s description, aggressively deprecates fields between versions. It pins a specific API version, adopted at each Business Central major release, and must uptake to the next version before support ends. Microsoft’s warning is blunt: “Fall behind, and the integration stops working.”
Both outcomes are real, and which one you get depends on how the connector is distributed. Shopify’s penalty for staying on retired resources is commercial: an app that keeps calling them is delisted from the Shopify App Store, users are blocked from installing it for a minimum of seven days, and merchants see warnings in the admin. A listed connector like Microsoft’s fails loudly, the way Microsoft describes. A custom app built for one merchant just falls forward and drifts.
The pin also runs behind current by design. The Shopify Connector released in April 2025 uses the Admin API version from January 2025, while the October release uses the API version released in July, and Shopify’s support timeline doesn’t align with the Business Central update period. Microsoft warned that the 2025 wave 2 connector relies on API version 2025-07, which it gave as supported until July 1, 2026, and Shopify’s own table puts that version’s end at July 16, 2026. Both dates are behind us, which makes it a clean illustration of the pattern rather than a live deadline.
Every connector has this property. Workato’s Shopify connector (opens in new tab) uses Shopify REST Admin API version 2026-01 and GraphQL Admin API version 2026-01, and 2026-01 stops being accessible on January 16, 2027. MuleSoft’s Anypoint Connector for Shopify (opens in new tab) is at version 1.1 and exposes operations provided by the Shopify REST Admin API, while Shopify has labeled REST a legacy API as of October 1, 2024 (opens in new tab) and required all new public apps to be GraphQL-only since April 1, 2025.
Neither of those is a defect, and vendors update connectors continuously. They’re both good due-diligence questions: which Shopify API version is your connector on today, what’s your committed lag behind current, and what happens to my store if you miss an uptake.
Who Owns the Quarterly Upgrade
That turns a maintenance detail into a contract question, and it needs answering before signature rather than at the first failed sync. Whoever owns the upgrade owns a recurring quarterly obligation for the life of the integration.
Put four things in writing. Which party performs the API version uptake, how many days before end of support it’s committed to happen, whether the work is inside a retainer or billed as change requests, and what the remedy is when a sync outage is caused by a missed uptake rather than by your code. We build these stacks, and the version pin is the line item most often absent from a scope of work.
What Mandatory Idempotency Breaks in an Existing Shopify ERP Integration
On December 12, 2025, Shopify announced a breaking change making the idempotency directive mandatory (opens in new tab) for a set of inventory and refund mutations in API version 2026-04. Seventeen mutations are named, including refundCreate, inventoryAdjustQuantities, inventorySetQuantities, inventoryMoveQuantities, inventoryActivate, the inventory transfer and shipment mutations, and locationActivate and locationDeactivate.
inventorySetScheduledChanges is on that list, and it’s worth checking before you build against it. Scheduled changes are being phased out, the mutation isn’t available in the 2026-07 Admin API, and scheduledChanges on InventoryLevel is deprecated. Pushing expected-receipt dates from a purchase order is a common reason ERP teams reach for it. Shopify’s replacement guidance is to skip the schedule and call inventoryAdjustQuantities, inventorySetQuantities or inventoryMoveQuantities when the change actually happens, which means the receipt date lives in the ERP and the sync fires on receipt.
The idempotency directive doesn’t show up as mandatory at the schema level, but calling these mutations without one results in an error at runtime. Schema introspection won’t warn you, codegen won’t warn you, and your type checker won’t warn you. It surfaces the first time you post a refund on the new version.
Shopify tracks idempotency keys for 24 hours (opens in new tab) from the original request. Retries that occur beyond this window, such as long-delayed job retries, have no idempotency protection and are treated as separate operations. Duplicates arriving while the first request is still in flight get IDEMPOTENCY_CONCURRENT_REQUEST instead of being processed.
The error your connector will actually hit is the other one. Shopify fingerprints each request’s parameters, so reusing a key with any changed value returns IDEMPOTENCY_KEY_PARAMETER_MISMATCH and the message “The same idempotency key cannot be used with different operation parameters.” Fingerprinting isn’t limited to values: parameter order can affect it, and Shopify’s instruction is to make sure your client orders input fields consistently to avoid unexpected parameter mismatches. A connector that rebuilds its payload from a hash or an ORM on retry can send byte-different, semantically identical input and get rejected.
Generate the keys deterministically and both problems get smaller. For deterministic scenarios such as background jobs and services, Shopify’s pattern is a UUID v5 derived from a stable job ID plus the mutation variables, which produces the same key on every retry of the same unit of work without storing it anywhere. Shopify’s advice to regenerate keys on page load and after successful operations is written for interactive apps, and an ERP connector should use the deterministic pattern instead. The 24-hour window still applies either way, so the key expires whether or not it’s reproducible.
If your architecture has a dead-letter queue that a human drains on Monday, that’s the design constraint to work around. A failed inventory adjustment replayed 48 hours later applies twice, and a replayed refundCreate refunds twice. Anything that can be retried outside the 24-hour window needs its own dedupe record on your side, keyed to the ERP document rather than to Shopify’s key.
Webhook Delivery: Why Every Shopify ERP Integration Needs a Reconciliation Job
Your endpoint acknowledges a webhook with a 200, and any response outside the 200 range, including 3XX codes, is treated as an error (opens in new tab). Shopify enforces a one-second connection timeout and a five-second timeout for the entire request. On failure, it retries 8 times over the next 4 hours, and after eight consecutive failures the subscription is deleted automatically if it was configured through the Admin API, with warning emails to the app’s emergency developer address.
Eight retries over four hours is the current figure. The older figure was nineteen retries over forty-eight hours, and building a recovery window around it gives you a margin that doesn’t exist. Four hours of endpoint downtime and the subscription is gone, not just the events.
Shopify also disclaims both ordering and delivery. It doesn’t guarantee ordering within a topic, or across different topics for the same resource (opens in new tab), so a products/update webhook might arrive before a products/create, and it recommends ordering off the X-Shopify-Triggered-At header or the payload’s updated_at. Webhook delivery isn’t always guaranteed either, and your app can miss or mishandle events for other reasons, such as handler failures or downtime. Shopify’s guidance is that your app shouldn’t rely on receiving data from Shopify webhooks.
So the reconciliation job isn’t a hardening task for phase two. Shopify’s own guidance is to use reconciliation jobs to periodically fetch data from Shopify so that your app stays consistent with Shopify’s data. Many queries support updated_at filters, so a job can fetch everything changed since its last successful run. In our experience this is the component most often cut to hit a launch date and rebuilt after the first bad week.
Deduplication has two headers and they do different jobs. Duplicate deliveries (opens in new tab) happen after network timeouts and retries. X-Shopify-Webhook-Id identifies an individual delivery, and X-Shopify-Event-Id correlates deliveries originating from the same merchant action, so multiple subscriptions on one topic produce different webhook IDs sharing one event ID. Dedupe on the webhook ID and correlate on the event ID.
And because you have five seconds total, queue the payload and acknowledge immediately, which Shopify calls a useful pattern for handling bursts of traffic.
Should you build on Shopify’s newer Events API instead? Not for production ERP work yet. Events is available today for a subset of topics (opens in new tab), and a subscription to an unsupported topic fails validation when you deploy. Shopify’s guidance is to migrate subscriptions one at a time as topic coverage expands, which makes it worth tracking and not worth pinning revenue to.
The other end of the wire disclaims ordering too. In Microsoft’s finance and operations apps, business events (opens in new tab) aren’t guaranteed to reach endpoints in the order the apps emit them, with defaults of three retries a second apart, ten endpoints per event and a ten-second HTTP timeout. Microsoft warns against using business events for data export, because they aren’t intended to carry large payloads to fulfill data export scenarios. Two event systems, two ordering disclaimers, one reconciliation job that has to be the actual source of consistency.
Throughput: The ERP Side Is the Bottleneck in Most Shopify ERP Integrations
The GraphQL Admin API’s rate limit is 100 points per second on Standard (opens in new tab), 200 on Advanced, 1,000 on Shopify Plus and 2,000 on Shopify for enterprise. Plus gets ten times the Standard throughput, which is one of the few places where the plan tier genuinely changes the engineering.
Shopify’s model is a leaky bucket with a calculated query cost, where every field carries an integer cost, a mutation defaults to 10 points, and a single query can’t exceed 1,000 points regardless of plan. At 1,000 points per second restore and 10 points per mutation, a Plus store sustains roughly 100 default-cost mutations per second.
That’s a ceiling rather than a working number, because the fields you select in a mutation’s payload add cost on top of the base 10. Shopify calculates cost twice and refunds the bucket the difference between the requested cost and the actual cost, so a lean payload gets most of it back.
Two other limits bite harder than the point budget on ERP-shaped traffic. Input arrays cap at 250: queries and mutations return an error if an input array exceeds 250 items, which sets your batch size for every quantities or variants array a sync job builds. And there’s a resource throttle that only large catalogs meet, where productCreate, productUpdate and productVariantCreate allow no more than 10,000 new variants per day once a store passes 500,000 product variants. Shopify exempts Plus from that one, but a non-Plus store doing a catalog backfill can hit a ceiling measured in days rather than seconds.
Rate limits are scoped to the combination of app and store, so calls from one app don’t affect the rate limits of another app, even on the same store. Every response carries extensions.cost.throttleStatus with maximumAvailable, currentlyAvailable and restoreRate. Shopify recommends using that metadata to manage your app’s behavior dynamically, with a recommended backoff of one second. Your integration can read its remaining budget from the response instead of guessing.
NetSuite Concurrency Is One Shared Pool
NetSuite governs concurrency at the account level (opens in new tab), and the account governance limit covers the total number of web services and RESTlet requests combined. SOAP web services and RESTlets draw from one budget, and a request can queue behind other SOAP or REST requests (opens in new tab) that are being processed. So your Shopify sync competes with the 3PL feed, the tax integration and every internal script.
There’s a carve-out that changes which architecture this applies to. NetSuite Connector is excluded from the scope of concurrency governance (opens in new tab), and by default it limits the concurrent sessions it uses to five, changeable only through NetSuite Customer Support. So the shared-pool problem is a custom-build and middleware problem. Choose the first-party connector and you trade the queueing risk for a ceiling you can’t tune yourself.
The base numbers are small. By service tier, Standard gets 5 concurrent requests (opens in new tab), Premium 15, and Enterprise and Ultimate 20 each, with development and partner accounts fixed at 5. Each SuiteCloud Plus license adds 10, so an Ultimate account with five licenses gets 70. Oracle also keeps a legacy tier table where Tier 1 starts at 15, and five licenses there works out to 65, so check which table your contract is on before you size anything.
You can also allocate concurrency to a specific integration (opens in new tab), which reduces the available limit for other integrations without a specific limit.
Set that against Shopify Plus at 1,000 points per second and the asymmetry is the whole capacity plan. Shopify will accept work far faster than a Standard-tier NetSuite account can absorb it, so queues, batching and backpressure belong on the ERP side of the middleware, not in front of Shopify.
SuiteScript governance tightens it further. record.load() on a standard transaction costs 10 usage units (opens in new tab) and Record.save() costs 20, and a RESTlet is capped at 5,000 units (opens in new tab) and a scheduled script at 10,000. A sales order load plus save is 30 units, so a single RESTlet invocation tops out around 166 order saves before it runs out of governance.
When you exceed the concurrency limit you get specific errors (opens in new tab): a RESTlet returns HTTP 400 with SuiteScript code SSS_REQUEST_LIMIT_EXCEEDED, request-level SOAP credentials produce ExceededRequestLimitFault with WS_CONCUR_SESSION_DISALLWD, and token-based SOAP auth produces ExceededConcurrentRequestLimitFault with WS_REQUEST_BLOCKED.
NetSuite runs its own optimistic locking, and with it enabled for custom records (opens in new tab) the user-facing strings are “Record has been changed.” and “Unable to save record. Record was changed by a different user. Please reload and try again.” The custom-record setting exists to make custom records consistent with the optimistic locking used generally for NetSuite standard records. Both platforms independently landed on optimistic locking, which tells you how often both were losing updates.
Business Central and Finance and Operations Throttle Differently
Business Central online allows a maximum of 5 concurrent requests (opens in new tab), a maximum batch size of 100, a maximum page size of 20,000 and a cap of 200 webhook subscriptions. The rate limit is per user: OData and SOAP together allow 6,000 requests within a five-minute sliding window.
Microsoft still publishes the older per-environment figures, 600 requests per minute in production and 300 in sandbox, but it has changed that per-environment limit to a per-user limit to increase throughput. Requests are throttled against the current per-user rate limits, which are strictly enforced, where the older per-environment limits weren’t strictly enforced.
So it’s one live limit and one superseded number that was never strictly enforced. Size against the per-user figure, and note that it scales with the number of identities you run the sync under rather than being fixed per environment.
Throttling returns HTTP 429, and request execution is limited to 10 minutes (opens in new tab) before the service returns a 504. Microsoft’s workaround is to spread your workload in smaller batches (opens in new tab) across multiple users or service principals. Extending APIs with additional fields (opens in new tab) isn’t currently possible in Business Central, so if you need it you must copy the AL code for the API and create a custom API based on that. Custom fields are a build, not a config toggle.
Finance and operations carries a published limit that is no longer enforced. Microsoft publishes user-based service protection API limits (opens in new tab) of 6,000 requests and 1,200 seconds of execution time per five-minute sliding window, plus 52 concurrent requests as an instantaneous ceiling with no window attached, all three measured per user per application ID per web server. As of March 31, 2023, those user-based limits are no longer being implemented in finance and operations apps environments, with the limits disabled on all environments and the enabling option removed in version 10.0.36.
What’s actually enforced is resource-based throttling on CPU and memory, surfacing as a high resource utilization error, plus priority-based throttling (opens in new tab) with Low, Medium and High levels and no published numeric thresholds.
Nine services are exempt, four of which matter to an integration: the Data Import/Export Framework, recurring integrations, Data Integrator and the finance and operations apps connector. That exemption list is the batch lane, and it’s the reason bulk ERP loads go through DIXF rather than through OData.
SAP is the outlier here, and its published numbers cover a different thing than you need. SAP does publish rate limits on Cloud Integration (opens in new tab), based on the number of requests per second and set per artifact, with separate ceilings per tenant and per user. Those limits cover the public management and design-time APIs, things like deploying a number range or listing message processing logs, not messages moving through a running integration flow.
SAP doesn’t publish the number an ecommerce sync actually needs, a rate limit for S/4HANA Cloud OData APIs. What SAP does publish on the runtime side describes tenant resource boundaries rather than requests per second. Sizing a sync against SAP means load testing your own tenant.
SKUs, Variants and Matrix Items: Where Shopify ERP Integration Mapping Breaks
SKU strings are case-sensitive at Shopify (opens in new tab), and SKUs returned from the stock endpoint are matched respecting case. Plenty of ERPs treat item codes as case-insensitive, so ABC-123 and abc-123 are one item on one side and two on the other. Normalize case at the boundary and log every mismatch rather than silently coercing.
The object model matters for where the SKU lives. A ProductVariant carries merchandising data and has a one-to-one relationship with an InventoryItem, the InventoryItem holds the SKU and physical product information, and an InventoryLevel connects one inventory item to one location. The join is ERP item to inventory item, not ERP item to Shopify product.
The ceilings don’t line up. Shopify announced on October 15, 2025 that it had raised the variant limit to 2,048 per product (opens in new tab), up from a historical limit of 100, and warned that merchants using apps that aren’t on the in-support GraphQL product APIs may have a downgraded or broken experience when creating or viewing products with more than 100 variants. In NetSuite, the maximum number of total combinations of matrix options is 2000 (opens in new tab), and the parent item doesn’t appear on transactions, only child items do. Two different models with two different ceilings, and the NetSuite figure counts option combinations rather than children.
Oracle offers an escape hatch called Virtual Variations (opens in new tab), a way to avoid rebuilding parent-child relationships in NetSuite when they already exist in the storefront. Oracle’s requirement is that the variant SKUs in the storefront have a corresponding item in NetSuite, which is a real trade: less matrix maintenance, more items in the item master.
Cross-Reference IDs in Both Directions
Never let the two systems find each other by string-matching SKUs at runtime. An app can create an app-owned metafield (opens in new tab) to track internal SKU codes, in the reserved app namespace. App-owned metafields are view-only in the admin by default and app-data metafields are hidden from it entirely.
Shopify will do the lookup for you once that metafield exists. productSet takes an identifier argument of type ProductSetIdentifiers, which specifies the identifier that will be used to look up the resource, and one of its options is customId, which lets you upsert (update or insert) a product using its custom ID as the identifier, where a custom ID is a unique metafield value. So the ERP can address a product by its own item number without first querying Shopify for a GID, and without the round trip that most connectors write by hand.
The pattern is otherwise symmetric. Store the ERP’s internal record ID on the Shopify object in an app-owned metafield, store the Shopify GID on the ERP record, and make every lookup use an ID rather than a business key. SAP does the same thing: its own Shopify integration package ships an iFlow named “Create Shopify Metafield Definitions.”
Default field mappings deserve a review pass for the same reason. Oracle’s shipped Shopify mappings (opens in new tab) send sku to itemId, order_number to tranid, the Shopify order id to a custbody_fa_channel_order field, and inventory_management to isdropshipitem. That last pair takes a Shopify inventory-tracking flag and lands it on a NetSuite drop-ship boolean, which is a semantic decision you’re inheriting rather than making. Read the default map line by line before go-live.
Customer Merges Invalidate Your Foreign Key
If the ERP stores a Shopify customer ID as its key, a merge can break it without an error anywhere. The two ID arguments to customerMerge (opens in new tab) don’t guarantee which customer is kept. Shopify’s selection order runs: an explicit overrideFields.customerIdOfEmailToKeep wins if it’s provided and valid, otherwise a single customer with an email address wins, otherwise account state and email marketing consent decide with an enabled account beating other states, and customerTwoId is kept as the fallback. The mutation is asynchronous and returns a job, and it requires the write_customer_merge scope.
Shopify provides customerMergePreview and a resultingCustomerId so you can find out which record survives before you commit. Subscribe to customer update events, resolve the surviving ID, and repoint the ERP key rather than letting a dead ID sit in your customer master.
Multi-Location Inventory and Split Shipments in a Shopify ERP Integration
Most ERPs model a sales order with one ship-from location and one fulfillment event. Shopify doesn’t. A fulfillment order (opens in new tab) is the strategy for how an order will be fulfilled, and there can be more than one fulfillment order for an order at a given location.
Shopify has four split-shipping triggers (opens in new tab) plus ship-and-pickup in one order: preorder items mixed with one-time purchases, subscription items mixed with one-time purchases, products shipping from different locations, and products in different shipping profiles, with a mixed shipping-and-pickup order treated as its own scenario. Shopify’s instruction to app developers is that if your app assumes a one-to-one mapping between an order, location, and fulfillment order, you update it to read fulfillment order data and the fulfillment order deliveryMethod.
Cancellation reshapes the graph again. After a fulfillment is canceled, a new fulfillment order is created from the canceled line items and goes back to the original location when the items are still stocked there. When they aren’t, the new fulfillment order is assigned to a location where the items are stocked, considering the shop’s fulfillment priority settings, and this might result in multiple newly opened fulfillment orders for different locations.
So the common case is quiet and the exception restructures the order. Your ERP integration has to handle a sales order whose shipment structure changed after it was already booked.
Location modeling is the other half. Each fulfillment service corresponds to an inventory location, Shopify’s guidance is that each physical warehouse should have its own fulfillment service, and Shopify requests all inventory data from a remote fulfillment service once every hour.
When multiple locations are enabled, Oracle’s connector has admins set different NetSuite locations to sync to different Shopify locations, and Microsoft’s Business Central connector (opens in new tab) pushes inventory levels from selected Business Central locations to multiple Shopify locations. The retail side of this problem is the same argument in a different context, which we covered in how Shopify POS changes unified commerce inventory.
NetSuite Connector currently doesn’t support transfer orders in Shopify (opens in new tab), so it can’t update incoming inventory quantities or create transfer orders in Shopify to determine incoming quantity. Oracle attributes that to Shopify not supporting transfer orders “in REST API,” and Shopify’s inventory transfer and shipment mutations exist in GraphQL today. That makes it a connector gap with a moving floor rather than a permanent platform limit, and a fair question to put to Oracle. Either way, if inter-location transfers are part of your operating model, the first-party connector doesn’t cover them now.
Refunds, Returns and Financial Write-Back in a Shopify ERP Integration
Refund write-back is where duplicates cost real money, and it’s now covered by the mandatory idempotency change since refundCreate is on that list of seventeen mutations. Returns have their own migration: API version 2025-07 introduced return processing (opens in new tab), which confirms return items, releases exchange items, and updates a merchant’s financial reports, and Shopify’s guidance is that existing return apps should migrate to use returnProcess to avoid disruptions and unexpected outcomes.
Oracle forces a direction here, which is a clean illustration of the system-of-record principle in vendor form. You can’t sync refunds both ways (opens in new tab) for a single account under one connector, so you must choose which way you want to sync your refunds, and Oracle’s Shopify best practice is to issue the refund in NetSuite and sync it out. An exchange is essentially a return and a new sale of the returned item, and return records themselves don’t sync, only the refund component.
Transaction type mapping is decided downstream of the connector. NetSuite Connector doesn’t control the record type that NetSuite creates when a transaction is billed. NetSuite looks at the field data on the sales order to determine the type of billed record to create.
The connector writes a sales order and populates the Billing subtab through your field mappings, and NetSuite reads Terms and Payment Method off that to pick between a cash sale and an invoice. On refunds, Oracle says the connector automatically detects whether to sync a credit memo or a cash refund.
The failure mode is a blank field rather than a wrong one. If neither the Terms nor the Payment Method field has a value (or Payment Option, if the Payment Instruments feature is enabled), NetSuite creates an invoice for the billed record. Oracle calls an unmapped payment method the most common reason an invoice appears where nobody expected one.
If your finance team expects every Shopify order to land as the same document type, that assumption breaks the first time a B2B order comes through on terms, or the first time a payment method has no mapping. The B2B side has its own order shape, which we walked through in when to move B2B operations onto Shopify Plus.
Order amendments have hard boundaries if the ERP is the master. Order editing (opens in new tab) requires the order to be in your store currency unless you’ve upgraded to Shopify Extensions in Checkout, isn’t available on archived orders or orders placed before January 1, 2019, and doesn’t allow line item quantity changes on prepaid subscriptions with multiple scheduled fulfillments. Editing an order also doesn’t modify the subscription contract itself.
Currency and timing are where a Shopify ERP integration produces month-end variances instead of errors. Every money field is a MoneyBag (opens in new tab) with presentmentMoney in the customer’s currency and shopMoney in the shop’s base currency, so posting the wrong one to the general ledger is a silent, compounding error. The Order object (opens in new tab) carries createdAt, set at checkout completion, and processedAt, which might not match the date and time when the order was created, plus updatedAt for change tracking.
Pick one field for period cutoff, document the choice, and filter reconciliation jobs on updatedAt while never using it for financial periods. A store selling into several regions compounds all of it, because every one of those fields now varies by market.
Reconciling to the bank needs an object most integrations skip. The ShopifyPaymentsPayout object (opens in new tab) includes a summary that breaks down fees and gross amounts by transaction type, such as charges, refunds, and adjustments, with a transaction type indicating whether funds move to the bank account or back to Shopify Payments. A deposit is net of fees and spans many orders, so order totals alone will never tie to the statement. Microsoft lists receiving payout information from Shopify Payments as a connector capability, which is a fair yardstick to hold any connector against.
Backfill and Steady State Are Two Architectures in One Shopify ERP Integration
Loading history and running daily sync are different problems, and trying to serve both with one code path is how projects lose a month. Historical backfill is a bulk, order-insensitive, throughput-bound job. Steady state is an event-driven, ordering-sensitive, latency-bound one.
Backfill also has a gate. By default you have access to the last 60 days’ worth of orders (opens in new tab) for a store, and reaching further back requires the read_all_orders scope, which you request from Shopify with a justification and which Shopify has to approve before you can add it alongside read_orders or write_orders. That’s an approval process with lead time, not a configuration flag, and it belongs on the project plan at kickoff.
For the extract side, bulk operations (opens in new tab) allow five concurrent operations per app per shop as of API version 2026-01, up from one, give a bulk query 10 days to complete, and produce a signed result URL that expires after one week. A bulk query supports five total connections nested no more than two levels deep. On the write side, bulk imports (opens in new tab) also went from one concurrent operation to five in 2026-01, require completion within 24 hours, cap the JSONL input at 100MB, and are limited to one connection field. bulkOperationRunMutation isn’t subject to the rate limits that single queries face.
That jump from one concurrent operation to five broke the query most backfill scripts poll with. currentBulkOperation is now deprecated in favor of bulkOperations with a status filter, because the current app’s most recent bulk operation stopped being a meaningful answer once five can be in flight at once. A backfill written before 2026-01 and left alone will poll the wrong job and report a finished import while four others are still running.
The API doesn’t process the contents of the JSONL file serially, and Shopify warns against relying on a particular sequence of lines and object order to achieve a desired result, so you can’t express create-then-update dependencies inside one file. And idempotency in bulk is applied per row in your JSONL input file (opens in new tab), not per the entire bulk operation, so reusing a single key across rows causes every row after the first to be treated as a duplicate.
That gives you a clean architectural fork. Backfill runs as staged bulk operations, one object type per pass, ordered by dependency across passes rather than within a file, with per-row idempotency keys generated from the ERP document ID. Steady state runs on webhooks with a queue, an idempotent handler, and the reconciliation job filtering on updated_at. On the ERP side the same split exists: the Data Import/Export Framework and recurring integrations are the exempt batch lane in finance and operations, while OData carries the transactional traffic.
Peak season is when the two paths collide, because a catalog refresh and Black Friday order volume want the same budget. We wrote about stress-testing a Shopify store before peak season, and the ERP sync belongs in that test plan alongside the storefront.
Sandbox and UAT: Where a Shopify ERP Integration Actually Gets Tested
Client transfer stores and development stores have opposite rules for this use case. On client transfer stores (opens in new tab) you can only install free apps and partner-friendly apps, custom and draft apps can’t be installed, and real transactions aren’t supported. A bespoke connector app is a custom app, so the store you hand to a client can’t host it during the build.
Development stores (opens in new tab) can be created on a Plus plan with enterprise features like Flow and Launchpad, and Shopify positions them as the place to build and test your apps. That makes them the right UAT environment for a bespoke build. Their limitations are different: testing runs through the Bogus gateway or your provider’s test mode, real transactions through active payment providers, Store Credit, and Gift Cards aren’t supported, dev stores can’t be converted to production stores, and they can’t be transferred to a client.
You can’t fully test payment-to-payout reconciliation before go-live, because payouts require real transactions. Sequence the financial sync last, run a parallel period where Shopify and the ERP both produce numbers and someone compares them, and don’t sign off the general ledger posting on synthetic data.
Non-production environments can also be a paid line item on the middleware side. Alumio lists a “Full OTAP street (production, UAT, sandbox)” (opens in new tab) under its Connect Core tier rather than its entry tier, which is a reasonable question to ask any iPaaS vendor before you assume UAT is free. Note too that Business Central’s non-rate limits are the ones to design your load test around, since the enforced per-user rate limit is a single figure with no sandbox and production split, while the 5 concurrent requests and the 100-record batch cap apply to both.
What Each ERP Vendor Actually Ships for Shopify Integration
So what’s the best ERP for ecommerce? There’s no vendor-neutral answer. The decision is usually settled before you start, because the ERP is already running your finance and supply chain.
What you actually have to establish is what your specific ERP and edition ships for Shopify, what it doesn’t, and what that gap costs you in middleware or custom development. Coverage is uneven in ways you can verify yourself in an afternoon.
Shopify describes its Global ERP Program (opens in new tab) as “A suite of certified enterprise resource planning apps to directly integrate with your Shopify store.” It contains five apps, a fraction of the broader inventory and ERP category (opens in new tab). The European ERP Program (opens in new tab) holds a further 13.
| App | Developer | Listed pricing |
|---|---|---|
| NetSuite ERP Connector | Oracle NetSuite | From $199.92/month |
| Dynamics 365 Business Central | Microsoft | Free to install |
| Brightpearl by Sage | Brightpearl | Free to install |
| Acumatica Cloud ERP | Acumatica | Free to install, additional charges may apply |
| Infor eCommerce Connector | Infor | Free |
Microsoft’s Business Central listing (opens in new tab) launched on April 29, 2022. A program badge tells you a connector exists, not that it covers your record set, so test it against your own objects before you commit.
Third-party connectors sit on the same platform as Oracle’s first-party one (opens in new tab). WebBee’s Robust NetSuite Integrator (opens in new tab) has published tiers at $80, $199.99, $300 and $500 per month, and TechMarbles’ NetSuite Integration – TM (opens in new tab) has tiers at $99 and $199 per month.
Oracle NetSuite
Oracle’s connector is the oldest of the first-party options, launched on the App Store on December 2, 2014, and its published tiers are B2C Standard at $199.92, POS at $249.92, B2C Premium at $249.92 and B2B at $916.58 per month, billed in USD on a 30-day recurring basis. Don’t budget off those numbers alone, because external charges may be billed by Oracle NetSuite separately from your Shopify invoice. The FarApp lineage is still visible in the product: admins log in at app.farapp.com (opens in new tab) during setup, and the default mappings (opens in new tab) ship custom fields named custitem_fa_shopify_handle, custitem_fa_shpfy_tags and custitem_fa_shpfy_metafld.
The connector’s limits decide scope: one store per connector, refunds syncing in one direction only, no transfer order support today, and metafields that are add and update only with deletion handled manually in Shopify.
None of those are defects. They’re published boundaries, and if your requirements sit inside them, this is the cheapest path with the clearest price.
Watch the transport timeline on the NetSuite side. Under Oracle’s SOAP removal plans (opens in new tab), new integrations should be built on REST web services starting with the 2026.1 release. From 2027.1 it will no longer be possible to build new integrations using SOAP web services, from 2027.2 only the last endpoint stays supported, and with 2028.2 all endpoints will be disabled and SOAP-based integrations will stop working. SOAP remains buildable until 2027.1, but a middleware vendor offering a SOAP-based NetSuite connector is offering a transport with a published shutdown date.
SuiteScript 2.1 is current, but the 2.x API version tag still resolves to 2.0 unless the “Execute SuiteScript 2.x Server Scripts As” account preference (opens in new tab) is set to 2.1. And SuiteCommerce (opens in new tab) is NetSuite’s own storefront product, not a Shopify connector, so a SuiteCommerce deck arriving mid-evaluation is answering a different question.
Dynamics 365 Business Central, and Not Finance and Operations
Business Central has a genuine first-party connector, preinstalled for new online environments, installable from Marketplace for existing ones, and published as open source in Microsoft’s BCApps repository (opens in new tab). It runs online only, not on-premises and not on Embed App deployments where the client URL uses the bc.dynamics.com format, and it requires both a Business Central and a Shopify license.
Keep the sync directions straight, because “bidirectional” isn’t accurate as a blanket description. Products and items sync both ways including images, variants, barcodes, vendor item numbers, tags and metafields, and customers and companies sync both ways with smart mapping on phone and email and B2B companies matched on tax or registration numbers. Inventory levels and posted sales invoices go from Business Central to Shopify. Orders come from Shopify to Business Central, from online stores, POS and B2B, carrying shipping costs, gift cards, tips, payment terms and transactions, PO numbers, fraud risk and return, refund or cancellation information, along with Shopify Payments payout data.
The Shopify connector doesn’t work with other Dynamics 365 applications, like Dynamics 365 Sales or Dynamics 365 Supply Chain Management. There’s no Microsoft first-party Shopify connector for finance and operations apps, so that project is middleware or custom, built against OData, custom SOAP or JSON services, and a read-only REST metadata service (opens in new tab), with business events for near-real-time push.
Dual-write (opens in new tab) is a bidirectional integration between finance and operations apps and Dataverse, and it has no role in a Shopify integration. And the Shopify connector in Power Platform is titled “Shopify (Independent Publisher) (Preview)” (opens in new tab), which routes it through the Independent Publisher program rather than shipping it as a first-party connector, throttled at 100 API calls per connection per 60 seconds, with four actions covering orders and customers and zero triggers. It’s not a product or inventory sync path.
SAP S/4HANA and SAP Business One Are Two Different Shopify Stories
SAP publishes Shopify integration content on its Business Accelerator Hub, and the catalog holds five Shopify packages: two published by SAP, plus packages from Incture Technologies, Flexso and PROCLANE. Every one of the five returns CertifiedBySap: false in the Hub’s own catalog data, SAP’s two included.
Keep that in proportion, because it’s the near-universal state of the catalog rather than a mark against Shopify content: across all 1,963 content packages the flag is false on 918, absent on 1,041 and true on four. So it isn’t a red flag, and it isn’t a green one either. For a buyer running diligence, “published by SAP” and “certified by SAP” are different statements, and neither the packages nor the Hub expose a certification you can cite.
SAP’s own package, S/4HANA Cloud Public Edition Integration with Shopify (opens in new tab) at version 1.1.3, is the substantial one, shipping eleven integration flow artifacts that cover product, stock, price and discount replication out to Shopify, B2C customers, orders, cancellations and returns coming back, and delivery details outbound. It targets S/4HANA Cloud public edition.
Private edition gets a different, thinner story. The private edition package (opens in new tab) is published by Incture Technologies and contains a single artifact, for pulling incremental data from Shopify into S/4HANA Cloud private edition, and writing data back to Shopify means reaching out to Incture. In the Flexso package (opens in new tab), only the S/4HANA-to-Shopify stock from MRP area flow has been made publicly available, with the rest available by contacting Flexso. Check what’s downloadable rather than reading a package’s advertised scenario list as shipped functionality, and apply that check to every vendor, not just these.
SAP also publishes a Shopify Adapter for SAP Integration Suite (opens in new tab) at version 1.4.0, which SAP says “facilitates the connectivity to Shopify APIs and accelerates the implementation time for Integrations with Shopify.” Whether it speaks REST or GraphQL is a question for SAP, and after October 2024 it’s the question that matters. And Flexso says its integration uses webhooks for real-time data synchronization, which means it inherits every delivery and ordering guarantee problem covered above. The reconciliation job is as mandatory on an SAP stack as it is on a custom build.
SAP Business One is a separate product, and no Shopify package exists for it in the Hub catalog at all. A Business One merchant connecting Shopify is on a third-party connector or on custom work against the Service Layer or the integration framework.
The SAP Business One app on the Shopify App Store (opens in new tab) is published by eShopSync, a third party, and syncs products and categories from Business One to Shopify in one direction. Note also that the DI API isn’t deprecated, and SAP publishes the functional limitations of the Service Layer versus the DI API, so read those before you settle on the Service Layer.
Mainstream maintenance for Business Suite 7 core applications (opens in new tab) runs to the end of 2027, and that window covers the latest three enhancement packages of SAP ERP 6.0 rather than every ECC estate, so check your EHP level before you assume you’re inside it.
Extended maintenance is then available for three years from the beginning of 2028 to the end of 2030, at a premium SAP puts at two percentage points on the maintenance basis for all support offerings in the Business Suite 7 scope, with customer-specific maintenance after that. SAP has also committed to at least one S/4HANA release being in maintenance until 2040. A brand on ECC is making an ERP decision and an ecommerce integration decision inside the same budget cycle, which is a reason to design the Shopify side to survive the ERP migration underneath it.
Native Connector or Middleware for Your Shopify ERP Integration
An ERP integration is usually a purchase decision before it’s a build decision: install a first-party app, hire an agency to build flows on an iPaaS, or write the connector yourself. Choose by scope rather than by philosophy.
If your ERP has a first-party connector, the record set matches what you need, and the connector’s limits are acceptable, the native connector is the cheapest path with the clearest published price. The moment your requirements exceed those limits, and multiple stores, bidirectional refunds, transfer orders, an unsupported record type or an ERP with no first-party connector all qualify, you’re buying middleware or building. Here’s the coverage picture.
| ERP | First-party Shopify connector | Coverage note |
|---|---|---|
| Oracle NetSuite | Yes, on the Shopify App Store | One store per connector, refunds one direction, no transfer orders today |
| Dynamics 365 Business Central | Yes, first-party and open source | Business Central online only, not on-premises, not Embed App |
| Dynamics 365 Finance and Operations | No | The Business Central connector doesn’t work with other D365 apps |
| SAP S/4HANA Cloud public edition | SAP-published Hub package, not SAP-certified | Eight business processes, eleven integration flows |
| SAP S/4HANA Cloud private edition | Partner package, not SAP-certified | One artifact, one direction |
| SAP Business One | No package in the Hub catalog | Third-party connector or custom Service Layer work |
The native connectors publish numbers on the App Store, while most iPaaS vendors publish little or nothing: Celigo (opens in new tab) names Standard, Professional and Enterprise editions with no currency figures, Workato (opens in new tab) publishes no plan names and no amounts at all, MuleSoft (opens in new tab) lists a starting price of $2,000 per month and sends every package to sales, and Patchworks (opens in new tab) marks Growth, Professional and Enterprise as enquire for pricing.
Two publish a plan price: Boomi (opens in new tab) lists Pay-As-You-Go at $99 per month plus usage, with every named subscription tier quote-only, and Alumio lists Connect Lite at EUR 499 per month, a starting price that scales by capacity.
The metering model prices your architecture rather than your revenue, which is why two brands at identical order volume get different invoices. Patchworks counts an operation each time it sends or receives a data payload, regardless of how many items that payload contains, with its tiers set at up to 150,000, 400,000 and 1,000,000 operations and three, four and eight connectors respectively. Under that model, a chatty sync that pushes one product per call costs several times what a batched one does at identical order volume, so batching design has a direct line to the invoice.
Celigo ships two distinct Shopify and NetSuite products, a managed Integration App (opens in new tab) and a template (opens in new tab), both on the Celigo platform, and the Integration App has thirteen prebuilt flows, covering orders, customers, customer deposits, items, item inventory, billing, fulfillment in both directions, cancellations, refunds and a payout-report add-on. Boomi’s current platform name is the Boomi Enterprise Platform and it markets a NetSuite and Shopify Integration Accelerator (opens in new tab). Workato has both a NetSuite SOAP and a NetSuite REST connector (opens in new tab) plus SAP RFC (opens in new tab) and OData.
Alumio lists integrations (opens in new tab) for SAP, S/4HANA, Business One, Business byDesign, ECC and Dynamics 365 Finance and Operations, and doesn’t list NetSuite. Every one of those is the vendor’s own description, and every one should be confirmed against your record set in a trial.
Marketplace presence varies too. Patchworks has a Shopify App Store listing (opens in new tab), free to install, while Celigo (opens in new tab) and Workato (opens in new tab) both list zero apps on the App Store. That’s a distribution choice rather than a capability signal, but it changes how billing, permissions and installation work.
What to Settle Before You Sign a Shopify ERP Integration Contract
All of the following is cheaper to answer in a scoping call than in a war room, and most of it never appears in a statement of work unless someone puts it there.
- Which system owns each object, and for the product record, which system owns each field
- Who performs the Shopify API version uptake, on what schedule, and under whose budget
- Whether the reconciliation job is in scope for launch or deferred, and what its cadence and alerting look like
- How your retry path stays inside Shopify's idempotency key retention window, and what happens to anything replayed later
- Whether your ERP tier's concurrency budget is shared with other integrations, and who arbitrates when they collide
- Which direction refunds flow, and what the process is when a refund is issued on the wrong side
- How historical order access is obtained, and when that request goes in
- Which environment hosts UAT, whether it can install a custom app, and which financial flows can't be tested there
- What the middleware meter counts, and what your projected sync volume does to it at peak
Answer those and the build is mostly mechanical. Skip them and the integration ships, syncs, and quietly disagrees with itself until someone in finance finds it.
If you’re scoping a Shopify ERP integration and want a partner who’ll put the version-pin question in the contract instead of the change log, that’s the kind of Shopify integration services work we do. Take a look at how BLKDG works as a Shopify Partner, then tell us about your Shopify and ERP stack and we’ll tell you where the gaps are before you spend anything.
Not sure where the gap is? That's exactly what the Digital Marketing Growth Audit is for.
A free, no-obligation look at where your site can win more traffic and conversions, with a clear digital marketing roadmap to get there. Just a straight read on where your digital presence stands and where it's headed.



