Most ERP integration projects are sold as a mapping exercise: here are Shopify's fields, here are the ERP's fields, draw lines between them. That framing is why so many of them quietly fail six months after go-live. The mapping is the easy part. The hard parts are deciding which system wins when two systems disagree, making every write safe to repeat when a job retries, designing around Shopify's API limits before you hit them at 3am on a sale day, and building the reconciliation that catches the drift you will absolutely accumulate. This is the honest engineering picture of connecting Shopify to NetSuite, SAP, Dynamics 365 Business Central, Zoho or Tally — what actually has to sync, where each flow breaks, and how to decide between a connector app, an iPaaS, and a custom build.
Signs you actually need this — and signs you do not yet
The clearest signal is that a human being is currently the integration. Someone exports a CSV of yesterday's orders, reformats it, and imports it into the ERP. Someone else updates stock in two places and occasionally forgets. That person is a single point of failure with a holiday schedule, and the cost of their mistakes scales with your order volume.
The other reliable signals are structural rather than volume-based:
- You oversell. Stock exists in the ERP or WMS, Shopify's number lags behind it, and customers buy things you cannot ship. This gets worse with every additional sales channel and every additional fulfilment location.
- Finance closes the month late because order, refund, tax and payout data has to be reassembled by hand before anything can be posted.
- Your catalogue lives in the ERP and product launches are bottlenecked on somebody re-keying attributes into Shopify.
- You run B2B and D2C off the same inventory pool with different price lists and different payment terms.
- You are on multiple storefronts — regions, brands, an expansion store — and the ERP has to see all of them as one business.
Now the counterweight, because this is a genuinely expensive project and plenty of brands buy it too early. You probably do not need a custom integration yet if: your SKU count is small enough that a daily export is genuinely fine; you fulfil from one location and sell on one channel; your ERP is an accounting package you use purely for filing rather than for operations; nobody has actually been harmed by the current manual process beyond mild annoyance; or your real problem is that your ERP data is a mess, in which case an integration will faithfully propagate that mess into Shopify at machine speed. Fix the data first.
And one honest note on sequencing: if a connector app covers 90% of your flows, buy the connector and build only the 10% it misses. Starting with a full custom build because it feels more serious is the most common way we see budget disappear before any operational problem gets solved.
Decide the system of record before anyone writes code
Every field that exists in both systems needs an owner, and "both" is not an answer. This is the decision that determines whether your integration is a data pipeline or a permanent source of arguments between operations and finance.
The default that works for most D2C and B2B brands looks like this. Products and pricing flow ERP to Shopify, because that is where costing, tax classification and item masters already live — with the deliberate exception of merchandising fields (descriptions, imagery, SEO, collections) which almost always belong to Shopify and should never be overwritten by an ERP push. Inventory flows ERP or WMS to Shopify, because the warehouse holds the physical truth. Orders flow Shopify to ERP, because that is where the sale happens. Fulfilment and shipment status flows back ERP to Shopify. Customers are the genuinely contested one, and usually need a rule rather than a direction: Shopify owns the storefront account and marketing consent, the ERP owns credit terms, tax registration and the billing entity.
Write this down as a table before you scope anything. For every field, name the owner, the direction, and the conflict rule — what happens when both sides changed since the last sync. The three defensible answers are "source system always wins", "last write wins by timestamp", and "flag it for a human". The indefensible answer is not deciding, which in practice means whichever job ran last wins, which means your data depends on cron timing.
Also decide what happens on delete, because nobody does and it hurts later. An item deactivated in the ERP should almost never delete a Shopify product with order history attached to it — archiving or unpublishing is nearly always the correct behaviour. If you want an outside opinion on where those lines should sit for your operation, that is exactly the kind of thing our strategy and consulting work exists to settle before build starts.
Products and catalogue: the sync everyone underestimates
Product sync looks simple until you meet the variant model. Shopify organises a product as a parent with option-based variants, each variant carrying its own SKU, barcode, price and inventory item. Most ERPs organise the same reality as flat item masters, sometimes with a matrix or configurator layer bolted on. Reconciling those two shapes — deciding what constitutes a "product" versus a "variant", and doing it consistently — is where the real work sits, and it is a modelling decision that is painful to change after launch.
A few concrete constraints worth knowing before you design the flow. Shopify's Admin API caps input arrays at 250 items and paginated result sets at 25,000 objects, so any full-catalogue operation has to be built around cursor pagination or bulk operations rather than a single large request. There is also a documented ceiling on variant creation at extreme scale: once a store passes 500,000 variants, it cannot create more than 10,000 new variants per day. If your ERP holds a genuinely enormous item master, filter server-side before you push rather than discovering that limit during a migration weekend.
The other trap is field ownership within a single object. An ERP push that overwrites the whole product overwrites your merchandising copy, your image ordering and your SEO fields every time an item cost changes. The fix is a per-field allowlist on the writing side, not a general instruction to "be careful". Metafields are the right home for ERP attributes that have no native Shopify equivalent — item class, HSN or tax codes, dimensional weight, supplier reference — and they keep those values queryable without polluting the fields your merchandising team owns.
Inventory: the hard one, and why
Inventory is where ERP integrations earn their fee, because it is the only flow where being wrong for ninety seconds costs you a real customer.
Start with the fact that Shopify's inventory model is richer than most people building against it realise. Shopify tracks quantities per inventory item per location across several named states: available, on_hand, committed, incoming, reserved, damaged, safety_stock and quality_control. on_hand is the sum of available, committed, reserved, damaged, safety_stock and quality_control. Critically, committed is read-only — it moves only when orders are created and fulfilled — which means an integration that blindly sets on_hand from the ERP is fighting Shopify's own order lifecycle. In almost every case the value your ERP should be writing is available, and the states you should be reading rather than writing are committed and incoming.
Then there is the race condition, which is the actual reason overselling happens. Between the moment your job reads a quantity from the ERP and the moment it writes that quantity to Shopify, customers keep buying. A naive absolute set silently reverses those sales. Shopify solves this properly with compare-and-swap semantics: inventorySetQuantities takes a compareQuantity, and the write only lands if the persisted value still matches what you expected. The related changeFromQuantity field fails with a CHANGE_FROM_QUANTITY_STALE error rather than overwriting a value that moved underneath you. There is an ignoreCompareQuantity escape hatch, and it should be treated as a last resort — Shopify's own guidance is that absolute sets are appropriate only when the calling system genuinely is the source of truth for that quantity. For everything else, inventoryAdjustQuantities with a delta is the safer primitive, because a delta composes correctly with concurrent activity in a way an absolute value never does.
Multi-location makes all of this combinatorial. Inventory is per location, so an ERP with three warehouses and a store needs a stable location mapping, agreement on which locations are sellable online versus reserved for retail or B2B, and a decision about whether you publish real quantities or a buffered number. Buffering — holding back a few units per SKU as a safety margin — is unglamorous and it is the single most effective anti-overselling measure available to most brands, because it converts a timing problem into a tolerance.
Finally, decide your cadence honestly. Full catalogue syncs on a schedule are simple and cheap but leave a window of staleness proportional to the interval. Event-driven deltas are fast but only as reliable as your event delivery. The pattern that actually works in production is both: event-driven deltas for speed, plus a scheduled full sweep for correctness, with the sweep treated as the authority when the two disagree.
Orders, fulfilment and returns
Order sync is the flow most connectors handle well, and also the one where partial correctness is most dangerous, because a duplicated order becomes a duplicated shipment.
The first design decision is when an order is eligible to move. Pushing on order creation is fast but means the ERP receives orders that may still be cancelled, edited or flagged for fraud review. Pushing on payment capture or on fraud clearance is slower but produces far fewer ERP-side corrections. There is no universally right answer; there is only a decision that operations has to consciously make and finance has to agree with.
Fulfilment has a specific technical shape on Shopify that older integrations get wrong. The legacy fulfilment endpoints keyed to an order ID were deprecated in the 2022-04 API version and removed in 2022-07; the current model is the FulfillmentOrder object, which represents the items in an order that are to be fulfilled from a single location — meaning one order can produce several fulfilment orders. The assigned location on the fulfilment order, not the older fulfilment_service field, is what tells you where the work is happening. If your ERP or 3PL supports partial and split shipments, this is the model you have to build against, and any integration still writing fulfilments the old way is running on borrowed time.
Returns and refunds are the flow that gets scoped last and causes the most finance pain. A refund in Shopify can be full or partial, can restock or not, and may or may not correspond to a physical return arriving at the warehouse. The ERP needs the credit note, the inventory movement and the payment reversal treated as three related but separable events. Modelling a refund as a single atomic thing is the shortcut that produces mismatched stock and unexplained credit balances a quarter later.
Customers, pricing and price lists
Customer sync is where you discover that the two systems disagree about what a customer even is. Shopify has customers who place orders; ERPs have billing entities with credit limits, tax registrations and payment terms, and one of those entities may correspond to many Shopify customers. For pure D2C this rarely matters. The moment you run B2B, it matters enormously.
Identity matching is the practical problem. Email is the natural key and it is unreliable — people change addresses, share them, and place guest orders. Any serious integration needs a durable external ID stored on both sides (a metafield on Shopify, a custom field in the ERP) written at first sync, with email used only for initial matching and never as the ongoing key. Deduplication rules should be explicit and, where confidence is low, should route to a human rather than merging automatically.
Pricing is genuinely well served by Shopify's newer primitives, and it is worth using them rather than inventing your own. Catalogs link a price list and a publication to a context — a market, a B2B company location, or an app — and price lists support either fixed prices or relative adjustments off the base price. That maps reasonably cleanly onto how most ERPs express customer-specific pricing: a base price plus a discount schedule per customer group. Push the structure, not the computed result, wherever you can, because pushing computed per-customer prices for every SKU multiplies your write volume by your customer count and turns a modest sync into a rate-limit problem. If B2B is the driver for your integration, our Shopify B2B and wholesale breakdown covers what the platform now does natively before you commission anything custom.
Financial data, invoices, and reconciliation
This is the section that gets cut from scope and added back at three times the cost after the first month-end close goes badly.
Financial sync is not just orders with amounts attached. It is order totals, line-level tax, shipping, discounts, gift cards, refunds, chargebacks, gateway fees and payout timing — and the last two are the ones that break naive designs, because the money that lands in your bank account is a net payout covering many orders, minus fees, on a different date than any of those orders. An ERP integration that posts order revenue but has no model for payouts leaves finance manually bridging the gap forever. Decide early whether the ERP receives per-order journal entries, summarised daily entries, or both, and whether payout reconciliation is in scope or handled elsewhere.
Then accept that the two systems will drift. Not might — will. Webhooks get missed, a job dies mid-batch, someone edits an order in the Shopify admin, a warehouse operator adjusts stock outside the ERP. Drift is not a sign of a broken integration; an integration with no drift detection is the broken one.
What a reconciliation layer actually needs is modest and rarely built: a scheduled comparison of the two systems on the fields that matter (order count and value per day, inventory quantity per SKU per location, customer count), a tolerance threshold, a report of the exceptions, and an alert when the exception count crosses a line. Most brands find their first real bugs within a week of turning this on. Where a warehouse already exists, this belongs alongside your other reporting — the same data warehouse and dbt setup that powers analytics is usually the cheapest place to run the comparison, because both systems are already landing there. Ongoing exception triage is genuinely operational work, which is why it tends to sit with an ongoing care and support arrangement rather than being handed back to the merchant at go-live.
The plumbing that decides whether it survives
Everything above is business logic. This section is the engineering that determines whether that logic still works on your busiest day of the year.
Rate limits. Shopify's GraphQL Admin API does not count requests; it prices them. Every query has a calculated cost — scalars and enums are free, objects cost points, connections scale with the first or last argument you pass, and mutations carry a baseline cost — drawn against a leaky bucket that refills at a published rate per second. Shopify documents that rate as 100 points per second on standard plans, 200 on Advanced, 1,000 on Plus and 2,000 on Enterprise, with a hard ceiling of 1,000 points for any single query regardless of plan. Two design consequences follow. First, request only the fields you need, because over-fetching literally costs you throughput. Second, read the throttle status Shopify returns in the extensions block of every response and use it to pace yourself, rather than firing requests until you get a 429 and backing off blindly. For anything catalogue-scale, use bulk operations instead: they sidestep the per-query cost ceiling entirely, run asynchronously against a JSONL file uploaded through a staged upload, and report completion through the bulk_operations/finish webhook. As of the 2026-01 API version an app can run up to five bulk mutation operations per shop simultaneously, where older versions allowed one at a time — worth checking against the version you are actually pinned to.
Webhooks versus polling. Webhooks are the right default and they are not a guarantee. Shopify delivers at least once, which means duplicates are normal, not exceptional; it expects a 2xx within a few seconds; and it retries a failing endpoint roughly nineteen times over about 48 hours before marking the subscription degraded and eventually removing it. That last behaviour is the one that bites: a bad deploy that returns 500s over a long weekend can leave you with no subscription at all and no obvious error, just silence. The production-grade pattern is to acknowledge the webhook immediately, enqueue the payload, process asynchronously, deduplicate on the X-Shopify-Webhook-Id header with a cache that outlives the full retry window, and run a scheduled reconciliation sweep that catches whatever the event stream dropped. Treat webhooks as an optimisation over polling, never as a replacement for a periodic sweep.
Idempotency. Every write must be safe to repeat, because it will be repeated. That means a deterministic external key on every object you create — an order reference, a fulfilment reference — checked before insert, so a retry updates rather than duplicates. Deltas are safer than absolute sets for inventory; compare-and-swap is safer still. Where the ERP supports it, use its own external ID field rather than maintaining a mapping table you have to keep consistent.
Partial failure. A batch of 500 orders where order 312 fails is the normal case, not the edge case. Shopify's bulk mutations help here by validating and executing each line independently and returning per-line errors in the output file rather than failing the whole operation, with partial results available even on failure. Your own jobs need the same property: per-record error capture, a dead-letter queue for records that fail repeatedly, and the ability to replay a single record without re-running the batch. A pipeline that can only be retried in full is a pipeline nobody will retry.
Observability. Log every sync attempt with a correlation ID that survives across both systems. When finance asks why an order from three weeks ago never reached the ERP, the answer should take minutes, not a day of log archaeology.
Three ways to build it, and when each is right
Off-the-shelf connector apps. For common pairings — Shopify to NetSuite, Shopify to Business Central — mature connectors exist and cover the standard flows well. They are the right answer when your data model is close to standard, your flows are the usual four (products, inventory, orders, fulfilment), and your customisation appetite is low. The economics are a monthly subscription plus a configuration or implementation engagement, and the honest trade is configurability: connectors handle their supported paths well and become frustrating exactly at the edges where your business is unusual. Do not treat that as a reason to skip them. Treat it as a reason to test your three weirdest requirements during evaluation rather than after signing.
iPaaS middleware. Platforms like Celigo, Boomi, MuleSoft, Patchworks, Alumio and Workato sit between the systems and give you a visual flow builder, prebuilt connectors, retry and error handling, and monitoring you would otherwise write yourself. This is the right choice when you have several systems rather than two — Shopify plus ERP plus WMS plus PIM plus marketplaces — or when you want the transformation logic owned by an internal ops team rather than a development team. Pricing is generally a platform subscription scaled by flows, connectors or volume, plus a meaningful implementation cost; vendors rarely publish full rate cards, so get quotes against your actual flow count rather than trusting a headline number from a comparison article. The real trade is that you have bought a dependency: platform-specific skills, another vendor relationship, and logic that lives somewhere you cannot fully unit-test.
A word on Zapier and similar lightweight automation tools, since they come up constantly. They are excellent for low-volume, one-directional, fire-and-forget tasks. They are the wrong tool for ERP sync, for structural reasons rather than snobbery: error handling is per-step with no transactional rollback, so a multi-record write that fails halfway leaves both systems in a partial state; nested data transformation and conditional field mapping are limited; payload sizes are capped; and many on-premise ERPs have no reliable trigger connector at all. Use them for notifications and internal alerting, not for inventory.
Custom-built integration. A purpose-built service, usually a custom Shopify app plus a worker tier and a queue, is right when your business logic genuinely is the differentiator: unusual allocation rules, a bespoke WMS, an ERP with heavy customisation, or scale where connector pricing stops making sense. You get exact control over conflict resolution, retry semantics, idempotency and reconciliation, and no per-flow licensing. You also own it — API version deprecations, ERP upgrades, on-call, the lot — and that ownership cost is the line item most first-time budgets miss entirely. Build custom when the integration is a competitive advantage or when nothing off the shelf can express your rules. Do not build custom to save a subscription fee; you will spend it several times over in maintenance. This is the work our deep engineering practice does most often, and the first thing we do on these projects is check whether a connector would have been enough.
ERP-by-ERP: what actually changes
NetSuite. The best-served pairing in the Shopify ecosystem, with the deepest connector and iPaaS options available. Technically you have SuiteTalk SOAP and REST web services plus RESTlets and SuiteScript for custom logic. The constraint that catches teams out is concurrency governance: NetSuite enforces an account-level concurrency limit shared across SOAP, REST and RESTlet calls combined, so your Shopify sync competes with every other integration and scheduled script in the account. You can allocate a slice of that limit to a specific integration under Integration Governance, and the governanceLimits operation lets your client discover what is available rather than guessing. Requests also time out after fifteen minutes, so long-running operations need chunking. Saved searches are usually a better extraction mechanism than record-by-record reads.
SAP. The most variable of the group, because "SAP" covers S/4HANA Cloud, S/4HANA on-premise and a long tail of ECC installations with a decade of custom ABAP. Your interface options are OData REST APIs (richest on S/4HANA, with native OData V4 support through the RAP framework), IDocs for asynchronous high-volume document exchange, and BAPI/RFC for synchronous calls into business objects. SAP Integration Suite on BTP is the sanctioned middleware, and SAP's own clean-core guidance pushes new integrations toward released APIs rather than direct RFC calls into custom code. Practical advice: the integration effort here is dominated by SAP-side availability of the right interfaces, not by anything on the Shopify side. Scope a discovery phase with an SAP functional consultant before committing to a timeline, and confirm early whether you are integrating with the ERP directly or through an existing middleware layer that already governs access.
Dynamics 365 Business Central. Genuinely pleasant to integrate with by ERP standards — native REST APIs and OData v4, with AL extensions available when you need custom endpoints. The documented service protection limits are the thing to design around: a default of roughly 600 requests per minute per environment with a cap on concurrent connections, HTTP 429 responses carrying a Retry-After header when you exceed them, and a ten-minute execution ceiling per request beyond which you get a gateway timeout. Honour Retry-After rather than implementing your own fixed backoff, and batch aggressively. Note also that Business Central and Dynamics 365 Finance and Operations are different products with different integration models — confirm which one you actually have before scoping.
Zoho. Common among Indian and mid-market brands, and the trap is quota rather than capability. Zoho Books and Inventory expose clean REST APIs, but the limits are tight: a per-minute ceiling around 100 calls per organisation, daily call caps that vary by plan from roughly a thousand on entry tiers to around ten thousand on the top ones, and a low concurrent-call soft limit. Quotas are also per product rather than pooled, so Books, Inventory and CRM each have their own. The design implication is unambiguous: batch everything, cache aggressively, never poll per SKU, and budget your daily call allowance as a real engineering constraint. A naive per-record sync will exhaust a day's quota well before the day ends. Verify the current limits for your specific plan against Zoho's documentation before you size anything.
Tally. Overwhelmingly the most common accounting system among Indian D2C brands, and the most architecturally awkward to integrate, for a reason that has nothing to do with data. Tally is a desktop application. It exposes an XML-over-HTTP interface on port 9000 for reading masters and posting vouchers, plus ODBC on the same port for read-oriented reporting, and TDL for custom objects and reports. But the interface is synchronous with no webhooks, ODBC is effectively read-only with no INSERT or UPDATE support, and TDL cannot receive inbound HTTP at all — Tally cannot call you, so every integration is a pull or push initiated from outside. Combine that with the machine typically sitting on an office LAN and often being switched off outside business hours, and the real architecture becomes clear: you need a small on-premise agent or a hosted Tally instance with a reliable network path, plus a queue that buffers Shopify events until Tally is reachable, plus voucher-level idempotency so a replayed batch does not double-post. TDL customisations also tend to break across major Tally versions, so treat upgrades as a scheduled integration event rather than a surprise. Done properly, order-to-voucher and GST-ready output are entirely achievable; done as a naive nightly script against a laptop, it fails the first time someone goes home early.
Our take
The projects that go well share one trait, and it is not budget. They start with a written agreement about who owns which field and what happens on conflict, and only then move on to APIs. The projects that go badly start with a connector demo, skip the ownership conversation, and discover eight months later that nobody can explain why the ERP and Shopify disagree about a thousand units of stock.
If you take three things from this: buffer your inventory rather than trusting perfect real-time sync, make every write idempotent because retries are guaranteed, and build reconciliation from day one instead of promising it for phase two. Those three decisions prevent more incidents than any amount of connector sophistication.
And genuinely consider the connector first. A large share of ERP integration requirements are met by an off-the-shelf connector plus a small amount of custom work at the edges, at a fraction of the cost and maintenance burden of a full build. Custom is the right answer when your rules cannot be expressed in someone else's tool — not by default.
If you want an honest assessment of which of the three approaches fits your operation, including the case where we tell you a connector is enough, talk to us about your ERP integration. We will look at your actual flows, your ERP's real interface options and your volume before recommending anything.
If any of this sounds like your situation, talk to us. We'll tell you exactly where your revenue is leaking and what it would take to fix it. Explore Strategy & Consulting →

