Shopify ERP Integration Patterns for Mid-Market Ops Teams
Six Shopify ERP integration patterns mid-market ops teams actually use, and when each fits NetSuite, SAP Business One, Acumatica, or Sage Intacct cleanly.
A "Shopify ERP integration" means very different things depending on which ERP sits on the other side of the wire. For a mid-market brand running on NetSuite, SAP Business One, Microsoft Dynamics 365 Business Central, Acumatica, QuickBooks Enterprise, or Sage Intacct, the choices are not a single shape. They are a small set of patterns that get mixed in different ratios. Shopify's own Admin API documentation treats every ERP destination identically: a GraphQL endpoint, webhook topics, and rate-limit budgets. The ERPs do not return the favor. Each one has its own integration shape, its own version of "real-time," and its own concurrency ceiling.
This post is organized by pattern, not by tool. We have shipped variants of all six on production deployments and the goal is to name what each pattern is good at, where it falls over, and how to combine them. The shopify erp integration patterns mid-market teams actually use are usually two or three of these layered together, not a single architecture.
The Business Problem
Mid-market commerce is the worst spot to integrate from. SMB tools assume one Shopify store, one location, and an accountant who imports CSVs into QuickBooks Online once a week. Enterprise tools assume a six-month systems integrator engagement, an internal data platform team, and a Boomi or MuleSoft license already on the books. The mid-market sits between those: GMV in the $5M-$80M range, 3 to 12 fulfillment locations, 800 to 8,000 SKUs, two or three sales channels, and a four-person ops team that owns inventory, fulfillment, and finance reporting between them.
Gartner's 2025 Magic Quadrant for Cloud ERP for Service-Centric Enterprises calls out the mid-market specifically as the segment where "integration complexity grows faster than headcount," and the trade press picks the same point up: a BetterCloud 2026 SaaS Management Report notes that mid-market firms run an average of 110 SaaS apps, with commerce and ERP as the two most frequently cited integration pain points.
$5M-$80M
the GMV band where Shopify integrations stop being a single workflow and start needing real architecture
The cost shape is consistent. A poorly architected integration burns roughly two finance-team days per month on manual reconciliation, drives 1-2% inventory drift, and produces a long tail of customer backorder emails. A well-architected one disappears into the background; the ops team stops thinking about it.
The mid-market also has a hiring problem that shapes the architecture choice. There are usually one or two engineers available to maintain whatever gets built. That constraint is what knocks several theoretically clean patterns out of contention.
The Technical Problem
Mid-market ERPs split into two camps by integration style. The webhook-friendly modern camp (NetSuite via SuiteScript 2.x and the REST web services, Sage Intacct's Web Services API, Dynamics 365 Business Central via OData v4, Acumatica's REST contract API) supports event-driven flows reasonably well, though each has rough edges. The polling-required camp (older NetSuite installs running SuiteScript 1.0, QuickBooks Enterprise via the IPP API and Sync Manager, on-premise Sage 100/300 instances, older SAP Business One installs without the Service Layer) forces batch sync as the default and webhook flows as a retrofit.
Shopify sits firmly in the modern camp. The 2024-10 Admin API stabilized the GraphQL surface, the webhook topic list now covers orders, fulfillments, inventory, customers, and products with at-least-once delivery, and the rate limit ceiling for GraphQL is calculated points per second per shop (the calculated cost varies by query depth, with most mutations costing 10-20 points against a 1000-point bucket). The mismatch between what Shopify pushes and what the ERP can accept is where every pattern below earns its keep.
One more constraint matters. A mid-market deployment usually inherits an ERP that someone else implemented. Custom fields exist. The chart of accounts was set up by an accountant in 2019. The item master has columns named things like SupplerPhone. You cannot redesign the schema; you can only build the integration to survive it.
Pattern 1: Event-Driven Sync (Webhook + Queue + Transformer)
The default shape. Shopify fires a webhook on orders/create, a queue (BullMQ on Redis, AWS SQS, or n8n's internal queue) buffers the event, a worker picks it up, an idempotent transformer maps the Shopify payload to the ERP schema, and a write hits the ERP. Done in under a second for most events, under five seconds even when the ERP is slow.
When it fits
Modern ERPs with webhook-symmetric APIs and tolerant rate limits. NetSuite with SuiteCloud Plus licenses (which raise the concurrency ceiling from 15 to 25+). Sage Intacct, which handles 100 calls per minute per company comfortably. Acumatica, where the contract-based REST API is built for this shape.
What breaks
Webhook ordering. Shopify does not guarantee order across topics; an orders/create and an orders/updated for the same order can arrive in reverse. Sales spikes that push concurrent worker counts above the ERP ceiling and burn through your rate budget on duplicate retries. We covered the specific NetSuite version of this in why your Shopify-to-NetSuite sync keeps breaking, and the same failure modes show up on Sage Intacct and Acumatica with different error codes.
// services/shopify-webhook.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { ordersQueue } from './queue';
export async function handleShopifyWebhook(req: Request) {
const raw = await req.text();
const sig = req.headers.get('x-shopify-hmac-sha256') ?? '';
const computed = createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET!).update(raw).digest('base64');
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(computed))) return new Response('bad sig', { status: 401 });
const topic = req.headers.get('x-shopify-topic')!;
const webhookId = req.headers.get('x-shopify-webhook-id')!;
const shopifyId = req.headers.get('x-shopify-order-id') ?? JSON.parse(raw).id;
// Idempotency key is the webhook id, not the order id; retries reuse the id.
await ordersQueue.add('process', { topic, shopifyId, raw }, { jobId: webhookId, attempts: 6, backoff: { type: 'exponential', delay: 2000 } });
return new Response('ok', { status: 200 });
}The handler does almost nothing on the request path. The 200 returns fast so Shopify does not retry. The real work happens behind the queue, where a worker can apply idempotency, retry against the ERP, and dead-letter what cannot be reconciled.
When it is wrong
Older NetSuite installs without RESTlets exposed for the records you need. QuickBooks Enterprise, where the Intuit Sync Manager runs on a Windows machine in someone's office and there is no webhook story at all. Any case where the ERP is the authoritative source of inventory and you actually need polling because the ERP changes outside Shopify's view (warehouse manager edits, EDI feeds, manual stock counts).
Pattern 2: Batch Delta Sync (Cron + Updated-Since Query)
A scheduled job runs every N minutes, queries the ERP for records changed since the last successful run (WHERE last_modified > :last_sync_at), and pushes them to Shopify via the Admin API. The other direction runs the same shape: pull Shopify orders modified since last sync, write them into the ERP in a single batch.
When it fits
QuickBooks Enterprise. Sage 100 and Sage 300 on-prem installs. Older NetSuite SuiteScript 1.0 environments that never got RESTlets stood up. Any ERP where the integration team does not control the deployment and cannot add webhook endpoints.
// jobs/erp-delta-sync.ts
import { erpClient } from './erp';
import { shopify } from './shopify';
import { readCheckpoint, writeCheckpoint } from './checkpoint';
export async function runInventoryDelta() {
const since = await readCheckpoint('inventory');
const batchSize = 200;
let cursor: string | null = null;
do {
const page = await erpClient.query({
table: 'item_inventory',
where: { updated_at: { gt: since } },
orderBy: 'updated_at asc',
limit: batchSize,
cursor,
});
if (page.rows.length === 0) break;
await shopify.inventoryAdjustments.bulkSet(page.rows.map(toShopifyAdjustment));
cursor = page.nextCursor;
// Checkpoint moves forward only after the Shopify write succeeds for the page.
await writeCheckpoint('inventory', page.rows[page.rows.length - 1].updated_at);
} while (cursor);
}The pattern looks dated but it is honest. The latency floor equals the cron interval, so a 5-minute job means inventory is up to 5 minutes stale. That tradeoff is fine for many mid-market brands, especially those with slow-moving SKUs or B2B order volumes.
What breaks
Clock skew between the integration host and the ERP, which causes records to be missed or re-pulled. A pure updated_at > last_sync query that does not handle the boundary correctly will drop records that were modified during the run. The fix is to query strictly less than NOW() - 1 second and store the high-water mark from the query, not the wall-clock time of the job.
When it is wrong
Inventory-tight businesses where a 5-minute delay causes oversells. Flash sales. Anything where Shopify needs to reflect ERP changes within the customer's checkout session.
Pattern 3: Hybrid (Event for Hot Paths, Batch for Reconciliation)
The architecture most production deployments converge on after the first major outage. Events drive the time-sensitive flows (orders Shopify-to-ERP, inventory ERP-to-Shopify on a tight loop, fulfillment status both ways). A nightly or hourly batch job handles masters (products, customers, price lists), bundle SKU expansion, and a strict reconciliation pass that compares both sides and corrects drift.
shopify webhooks ──► queue ──► transformer ──► erp (orders, fulfillments, inventory)
│
erp polling ──────► delta query ──► transformer ──► shopify (inventory, masters)
│
nightly reconciler
├── product diff
├── inventory diff (tolerance per SKU class)
└── orphan order sweep
The hybrid wins because the failure modes of the two patterns are uncorrelated. A webhook that misses fires gets caught by the reconciliation. A reconciliation that runs on a bad dataset gets overridden by the next live event. Neither layer has to be perfect.
What it costs
More moving parts. Two transformer codebases (or one transformer with two entry points), two checkpoint stores, two alerting paths. The team has to understand both. New engineers take longer to ramp. The benefit is that the integration survives.
The reconciliation matters more than the live sync
We have said this before in other forms and it is still true. The live sync is satisfying to build. The reconciler is what makes the integration trustworthy. A Callout is warranted here.
Build the reconciliation job first, even before the live event sync. A nightly batch with 8-hour latency is a working integration. A live sync without reconciliation is a system that drifts silently until month-end close.
When it is wrong
Very low-volume integrations (fewer than 50 orders per day) where the operational overhead of running two patterns is not worth the resilience. For those, pure batch is usually enough.
Pattern 4: iPaaS-Anchored (Celigo, Workato, Boomi in the Middle)
A connector platform sits between Shopify and the ERP. Celigo's integrator.io has pre-built flows for NetSuite, Acumatica, and QuickBooks. Workato has recipes for Dynamics 365 Business Central, SAP Business One via the Service Layer, and Sage Intacct. Boomi processes cover the same ground with more enterprise positioning. The integration team configures rather than codes.
When it fits
The 80% case. Standard order, fulfillment, and inventory mapping. No exotic custom fields. No multi-entity NetSuite consolidation. Ops team that includes someone comfortable with low-code tools and willing to learn the platform's quirks.
Real cost numbers, since this is the question every operator asks: Celigo's NetSuite + Shopify integration app starts around $2,000/month and climbs toward $5,000-$8,000/month at mid-market volume, per Celigo's published pricing pages. Workato sits in a similar band at the workspace + recipe level. Boomi is harder to price publicly but lands in the same neighborhood for comparable workloads.
What breaks
Custom logic the visual builder cannot express. A bundle expansion rule that depends on inventory availability at multiple warehouses, a tax calculation that requires a service call to Avalara before posting to the ERP, a customer-tier discount that needs to read three tables. We covered the specific tradeoffs of Celigo vs Boomi vs custom build in choosing a Shopify-NetSuite connector; the same shape applies to any of the iPaaS players.
The other failure mode is the bill. Mid-market operators frequently end up with both Celigo and a half-dozen Shopify apps doing pieces of the same job, and the running total surprises everyone. The cost dynamic is the focus of the $400/month trap.
When it is wrong
Audit and compliance constraints that require the integration to live inside your own VPC. Custom logic that needs to be unit-tested and reviewed in PRs. Anything where the integration team needs to own the source code because the business depends on changing it weekly.
Pattern 5: Custom Middleware (In-House Service)
A first-party service, usually Node or Python, that owns the integration end to end. Subscribes to Shopify webhooks, writes to the ERP, runs its own reconciliation, persists its own state, ships its own observability. The team owns the codebase and the on-call rotation.
When it fits
When you have to. Specifically: when there is custom logic the iPaaS cannot express, when the data flow needs to integrate with internal systems (a custom warehouse management tool, a homegrown PIM, a separate fraud engine), when the cost of the iPaaS exceeds the salary of a senior engineer, or when compliance requires the data to never leave your environment.
We have shipped custom middleware on Node.js 20 with TypeScript, Postgres for state, BullMQ on Redis for queues, and either Railway, Google Cloud Run, or AWS ECS for hosting. The shape is consistent across deployments: a Shopify webhook receiver, a worker pool, a state store, an ERP client with rate-limit governance, a reconciler, and a dashboard for ops.
The honest cost
A custom middleware build is 2-4 engineering months for the first version, then 20-40% of one engineer's time for ongoing maintenance and feature work. There is an on-call cost: someone wakes up when the integration breaks at 2am during a flash sale. There is an upgrade cost: Shopify API version bumps every quarter, NetSuite releases twice a year, SAP B1 patches break the Service Layer occasionally. Plan for this. Do not pretend the work ends at launch.
When it is wrong
When the iPaaS would do the job and the team does not have an engineer to dedicate. Building custom because it feels more technical, then leaving it under-maintained, is the worst possible outcome. The integration goes dark, the ops team loses trust, and the eventual rebuild costs more than the original would have.
Pattern 6: CDC + Read Replica (Analytics, Not Sync)
Change Data Capture from Shopify and the ERP into a warehouse (BigQuery, Snowflake, ClickHouse), where dashboards and analytics queries run against a read replica that is eventually consistent with both source systems. Tools like Hevo, Fivetran, and Stitch handle the extraction.
When it fits
Analytics. Executive dashboards. Cohort analysis. Anything that reads from both Shopify and the ERP and does not write back to either. The CDC pattern is excellent for getting Shopify order data and ERP financial data into the same SQL surface so the finance team can stop exporting CSVs.
When it is wrong
This pattern is not order-of-record sync and should never be used as one. The latency is measured in minutes to hours. The replica is read-only by design. Treating CDC output as the source of truth for inventory or order state will produce silent inconsistencies that no one notices until the warehouse picks the wrong SKU. Use it for what it is good at and pair it with one of the first three patterns for the transactional path.
Cross-Cutting Concerns
Five things apply across every pattern above. None are optional.
Idempotency. Every write to the ERP needs an idempotency key derived from the source event, not from the HTTP call. Shopify's at-least-once webhook delivery and the team's own retry logic will both produce duplicate attempts. The fix is the same in every pattern: an externalId field on the ERP record, an upsert instead of insert, a unique constraint on the integration's own state table.
Observability. Structured logs per event, per transformation, per ERP call. Track event id, source system, target system, latency, and outcome. Alert on the absence of events (a Shopify topic that has not fired in 30 minutes when it usually fires every 5).
Governance. Most mid-market ERPs cap concurrency at the account level. NetSuite is 15 for standard accounts, more with SuiteCloud Plus. Sage Intacct documents 100 calls per minute per company. SAP Business One Service Layer is configurable but defaults to single-digit concurrency. The integration must respect these or it will starve other consumers (the finance team's CSV import, the BI tool's scheduled queries).
Drift detection. A nightly diff of inventory counts and order counts between Shopify and the ERP. Tolerance per SKU class. Alert when drift exceeds tolerance. This is the single highest-leverage piece of code in the entire integration; build it early.
Schema legacy handling. The mid-market ERP has fields named oddly, custom fields the original implementer added, and columns no one remembers the purpose of. The transformer needs an explicit mapping layer that is reviewed and version-controlled, not heuristic guessing. We use a small in-repo mapping module with one entry per logical field; new fields are added with a PR and a test.
Observations: What Worked, What Didn't, What We'd Do Differently
What worked. The hybrid pattern. Almost every production deployment we run is some shape of webhook-driven hot path with a batch reconciler underneath. The hot path catches 98% of events on the first try; the reconciler catches the rest. Building both, even when the live sync feels complete, has saved month-end every single time.
What didn't. Trying to do all six patterns inside a single iPaaS recipe. Celigo and Workato are good at the iPaaS pattern; they are not good at the custom-middleware pattern. Pushing complex bundle expansion or multi-warehouse allocation logic into a visual builder produces unmaintainable flows that no one wants to touch. Pull that logic into code.
What we'd do differently. We would standardize the state store earlier. Different deployments evolved different Postgres schemas for tracking order state, and consolidating them later was a multi-week refactor each time. If we were starting fresh, we would publish an internal package with a versioned order_state schema, a checkpoint table, and an event log, and reuse it across clients. The variance was not adding value.
We would also write the runbook before the integration. A 10-line document that says "if the reconciler shows drift above 2%, do X, Y, Z" gets used at 11pm on Black Friday by whoever happens to be on call. We have written it after the fact every time and it has cost us. Write it first.
References
- Shopify Developer Documentation, "Admin GraphQL API Reference," 2024-10. https://shopify.dev/docs/api/admin-graphql
- Shopify Developer Documentation, "Webhook Topics," 2024-10. https://shopify.dev/docs/api/admin-graphql/2024-10/enums/WebhookSubscriptionTopic
- Gartner, "Magic Quadrant for Cloud ERP for Service-Centric Enterprises," October 2025. https://www.gartner.com/en/documents/erp-cloud-service-centric
- BetterCloud, "2026 State of SaaSOps Report," 2026. https://www.bettercloud.com/monitor/
- Celigo, "Integrator.io Pricing and Plans," 2026. https://www.celigo.com/pricing/
- Workato, "Recipes for Microsoft Dynamics 365 Business Central," 2026. https://www.workato.com/integrations/dynamics-365-business-central
- NetSuite, "SuiteCloud Concurrency Governance," 2026. https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158309217005.html
- Sage Intacct, "Web Services API Developer Documentation," 2026. https://developer.intacct.com/web-services/
- Microsoft Learn, "Dynamics 365 Business Central OData Web Services," 2026. https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/odata-web-services
- Acumatica, "Contract-Based REST API Reference," 2026. https://help.acumatica.com/Help?ScreenId=ShowWiki&pageid=intro-rest-api
Stack: Node.js 20, TypeScript, Postgres, BullMQ on Redis, Shopify Admin API 2024-10, NetSuite REST + SuiteQL, Sage Intacct Web Services, Acumatica REST contract API. Deployed on Railway, Google Cloud Run, and AWS ECS depending on client. Reconcilers run as scheduled jobs; live syncs as long-running workers behind Shopify webhook endpoints.
Want to build something like this?
We design and ship AI products, automation systems, and custom software.
Get in touch