11 min read
shopify
erp
ecommerce
integrações
arquitectura

Synchronise stock between Shopify and ERP in real time

How to synchronise stock between Shopify and ERP with webhooks, queues, idempotency, reconciliation and clear rules about the source of truth.

Duas pessoas caminham num corredor luminoso junto ao armazém, com papéis de stock na mão
  • Shopify should not be treated as an ERP. It should be the transactional storefront, while the ERP remains the source of truth for physical stock, purchasing, reservations and warehouses.
  • “Real time” in stock rarely means immediate. A healthy target is to propagate critical changes in 2 to 10 seconds and reconcile discrepancies in 5 to 15 minute cycles.
  • Webhooks are mandatory, but not enough. You have to assume duplicate events, out of order events, temporary failures and API limits.
  • The right architecture involves a queue, idempotency, explicit SKU and location mapping, and an independent reconciliation job.
  • The biggest trap in production is synchronising absolute quantities without understanding reservations, locations and orders that have not yet been shipped.

First decide who controls stock

The first decision is not technical. It is operational.

In an integration between Shopify and ERP, someone has to be the source of truth. If you do not make this decision in writing, you will end up with two teams correcting stock manually in two different systems, and the integration will become responsible for every discrepancy.

My opinion is simple: in most retailers and B2B operations with a warehouse, the ERP should control available stock. Shopify should receive availability for sale. It should not try to calculate real stock from orders, returns, transfers and purchase receipts.

There are exceptions. A small shop, with few SKUs and no mature ERP, can leave Shopify as the main source for a while. But when multiple warehouses, purchasing, batches, reservations for marketplace, POS, returns and B2B orders enter the picture, Shopify stops being the right place to decide availability.

The most important distinction is this:

  • Physical stock: units existing in a warehouse.
  • Reserved stock: units committed to orders not yet shipped.
  • Available stock for sale: physical minus reservations, minus buffers, minus quality blocks.
  • Stock published in Shopify: the number the customer sees or that checkout allows them to buy.

In practice, Shopify only needs the last value. The ERP should calculate the rest.

If you sell 10 units of an SKU, have 3 reserved for a wholesale order, 2 in quality control and want to keep a safety buffer of 1 unit, Shopify should not receive 10. It should receive 4.

It sounds basic, but this is where many integrations fail. They synchronise “stock on hand” instead of “available to sell” and then wonder why they oversell.

The data model that avoids pain

Before talking about webhooks and APIs, identifiers need to be aligned.

In Shopify, inventory is not just “product with SKU”. The relevant model includes Product Variant, Inventory Item, Location and Inventory Level. The official documentation is at https://shopify.dev/docs/api/admin-graphql and the inventory section deserves careful reading before writing code.

The ERP, on the other hand, may think in terms of items, warehouses, batches, units of measure and stock statuses. The bridge between the two worlds should be explicit.

At a minimum, you need this mental table:

  • SKU in the ERP.
  • Variant ID in Shopify.
  • Inventory Item ID in Shopify.
  • Location ID in Shopify.
  • Warehouse or set of warehouses in the ERP.
  • Publishing rule, for example, “publish sum of A1 and A2 with a buffer of 2 units”.
  • Last published quantity.
  • Last synchronisation date.
  • Link status, active, suspended, mapping error.

Do not rely only on SKU as a unique key. In production, duplicate SKUs happen. Old variants remain archived. Products are recreated manually. Merchandising apps change variants. Migrations import products with inconsistent fields.

The rule I usually apply is: SKU is for discovery and auditing, not for definitive identity. The definitive identity in Shopify should be the Inventory Item ID and Location ID. In the ERP, it should be the internal ID of the item and warehouse, not just the visible code.

You should also decide how to handle bundles and kits. If you sell a “pack of 3” in Shopify, but the ERP only knows the individual unit, there is no simple 1 to 1 synchronisation. You need an availability rule: the pack stock is the lowest possible whole number based on the components. If you have 10 T-shirts and 4 caps, and the pack takes 1 T-shirt and 1 cap, you can only sell 4 packs.

This logic should not be scattered across webhooks. It should live in an availability layer, testable and auditable.

For near real time synchronisation, the architecture I recommend is this:

  1. Shopify sends webhooks for orders, cancellations, returns and inventory changes.
  2. ERP sends stock events when there are receipts, transfers, adjustments, reservations or shipments.
  3. An integration layer receives events and validates the signature.
  4. The events go into a queue.
  5. Workers process events idempotently.
  6. The availability layer calculates the publishable value.
  7. The integration updates Shopify via the Admin GraphQL API.
  8. A reconciliation job compares ERP and Shopify at regular intervals.

Do not put heavy logic inside the webhook endpoint. The endpoint should validate, persist and respond quickly. Aim for p99 below 500 ms on the endpoint. Processing may take longer, but should happen outside the original request.

A simple and sufficient stack could be:

  • Node.js 22 LTS or Python 3.12 for the integration.
  • PostgreSQL 16 for mappings, idempotency and auditing.
  • Redis with BullMQ, SQS, Pub/Sub or an equivalent queue for asynchronous processing.
  • Workers separated by event type.
  • Observability with structured logs, metrics and alerts.

You do not need microservices for this. You need clear boundaries. A well written integration service is better than five small services without idempotency.

A simple idempotency pattern in PostgreSQL helps a lot:

CREATE TABLE integration_events (
  source text NOT NULL,
  event_id text NOT NULL,
  topic text NOT NULL,
  received_at timestamptz NOT NULL DEFAULT now(),
  processed_at timestamptz,
  status text NOT NULL DEFAULT 'pending',
  payload jsonb NOT NULL,
  PRIMARY KEY (source, event_id)
);

When you receive a webhook, you try to insert it. If it already exists, you do not process it again. This avoids duplicating adjustments when Shopify or the ERP repeat events. Webhooks should be treated as “at least once”, not as “exactly once”.

For Shopify, you should validate HMAC in webhooks. The official documentation is at https://shopify.dev/docs/apps/build/webhooks. Without validation, anyone who discovers the endpoint can simulate a stock change.

Webhooks, GraphQL API and the limits that matter

Shopify has useful webhooks for inventory and orders, but you should not expect a perfect narrative of events. Events can arrive out of order. Some may fail temporarily. Your endpoint may be down. An app may be removed. A subscription may change.

The exact topics vary with the type of integration, but you normally look at:

  • Orders created, paid, cancelled and updated.
  • Fulfillments created or updated.
  • Refunds and returns.
  • Inventory levels updated.
  • Products and variants updated, to catch SKU and tracking changes.

To write stock to Shopify, use the Admin GraphQL API. The REST Admin API was marked as legacy for new public apps from 2024, so I would not start a new project based on REST if the operation is going to grow.

Pin an API version, for example 2025-10, and schedule quarterly reviews. Shopify versions the API quarterly and removes old versions. Do not leave this to chance.

On limits, there are two practical points:

  • The GraphQL Admin API uses a query cost model, not just number of requests.
  • In standard shops, the bucket and restore rate can limit mass updates. Check https://shopify.dev/docs/api/usage/rate-limits before designing aggressive jobs.

This changes decisions. If you have 50 000 SKUs and receive a full import from the ERP every 5 minutes, you cannot simply fire 50 000 updates at Shopify without control. You have to compare differences and only publish real changes.

Rule of thumb: never send Shopify a quantity that is equal to the last published quantity. It sounds like a detail, but it reduces load, noise, cost and rate limit risk.

Another important decision is choosing between adjusting and setting absolute quantities. For synchronisation from ERP, I prefer setting the absolute available quantity per location, calculated by the ERP. Incremental adjustments are dangerous when events are lost or out of order. If the “minus 2” event arrives twice, you are wrong. If you set “now it is 18”, the operation is easier to reconcile.

Note: this does not mean ignoring concurrency. If Shopify also changes stock through sales, returns or apps, you have to understand whether those changes should go back to the ERP or whether the ERP rewrites availability. Once again, source of truth.

Comparison of the possible approaches

There are three typical routes. None is universal.

  1. Simple polling

Polling reads stock from the ERP or Shopify every X minutes and updates the other side.

Pros:

  • Easier to implement.
  • Does not depend on well configured webhooks.
  • Useful as a first version for small catalogues.

Cons:

  • Worse latency. If you run it every 10 minutes, you may sell stock that no longer exists.
  • Unnecessary load.
  • Weak for peaks, campaigns and flash sales.
  • Tends to hide errors until it is late.

I would only use polling as the main mechanism in small operations, with low volume and low criticality. Even then, I would keep reconciliation and logs.

  1. Webhooks with queue

This is the balanced option for most operations.

Pros:

  • Low latency. 2 to 10 seconds is realistic when ERP and Shopify cooperate.
  • Better API usage.
  • Recovers better from temporary failures.
  • Allows retries, dead letter queue and auditing.

Cons:

  • More engineering.
  • Requires idempotency.
  • Requires monitoring.
  • Forces you to think about the data model.

For a serious shop, this is the right foundation.

  1. iPaaS or low-code middleware

Tools such as Celigo, Make, Zapier, n8n or specific ERP connectors can speed things up significantly.

Pros:

  • Fast start.
  • Less custom code.
  • Good option when the flow is simple.
  • May be enough for teams without internal technical capacity.

Cons:

  • Complex stock logic becomes difficult to maintain.
  • Limited or expensive observability.
  • Reprocessing and idempotency are not always clear.
  • Costs rise with volume.
  • Debugging edge cases can be painful.

My position: iPaaS is good for validating flows and for administrative integrations. For critical stock availability, with multiple warehouses and a direct impact on checkout, I prefer a custom integration or at least a custom decision layer.

Common traps in production

The first trap is assuming that “stock” means the same thing in both systems. It does not. The ERP may show physical stock. Shopify needs stock available to sell. Finance may want valued stock. Operations may talk about shippable stock. These are different numbers.

The second is forgetting locations. Shopify works with inventory levels per location. If you publish everything to a generic location, you lose the ability to represent warehouses, physical shops and fulfilment rules. If you have shops with POS, this becomes even more sensitive.

The third is treating webhooks as an ordered sequence. They are not a perfect queue of business events. They are notifications. You have to be able to recalculate current state when you receive a suspicious event. That is why reconciliation is mandatory.

The fourth is not handling returns and cancellations. A cancelled order may or may not return stock, depending on the operational status. A return may arrive damaged and not become available again. A financial refund is not the same as a physical receipt in the warehouse.

The fifth is ignoring buffers. If you sell on Shopify, marketplace, wholesale and in a physical shop, publishing 100 per cent of available stock on Shopify is asking for overselling. A fixed buffer of 1 or 2 units may be enough in small catalogues. In larger operations, the buffer should vary by sales velocity, lead time and stockout risk.

The sixth is not having an audit screen. When someone asks “why did this SKU end up with 0 in Shopify?”, you cannot spend an hour searching through raw logs. You need history by SKU: event received, calculation made, previous quantity, new quantity, API response and error if one existed.

Minimum metrics I would put in place from day one:

  • Latency between ERP event and Shopify update, p50, p95 and p99.
  • Number of pending events in the queue.
  • Number of failures by topic.
  • Number of SKUs without mapping.
  • Differences found in reconciliation.
  • Rate limit consumed in the Shopify API.
  • Updates ignored because there was no quantity change.

Without this, the integration appears to work until the first sales peak.

Reconciliation is mandatory, not plan B

Even with webhooks, queues and good engineering, you will have discrepancies. The question is not “if”. It is “when” and “how you find out”.

A reconciliation job should run in the background and compare the availability calculated by the ERP with what is published in Shopify. For small catalogues, it can run every 5 or 15 minutes. For large catalogues, segment by recently changed SKUs, active products and high turnover products. A daily full scan is also healthy.

The process should be conservative:

  1. Read availability from the ERP.
  2. Read relevant inventory levels from Shopify.
  3. Compare with zero tolerance for simple products.
  4. Generate a correction only when the difference is real.
  5. Save an audit trail.
  6. Alert if the difference is recurring or above a threshold.

During campaigns, I would reduce the reconciliation interval for critical SKUs. If you have products with 300 orders per minute during a promotion, waiting an hour to discover a discrepancy is too late.

You should also have a degraded mode. If the ERP is unavailable for 20 minutes, what do you do? Do you continue selling with the last known stock? Do you set critical products to 0? Do you increase the buffer? This is a business decision, but it has to be implemented before the failure.

For many businesses, the right rule is: if the ERP does not respond and stock is low, reduce availability in Shopify. It is better to lose a few sales than to sell what you cannot ship.

Conclusion

Synchronising stock between Shopify and ERP in real time is not connecting two endpoints. It is defining the source of truth, modelling availability, processing events with idempotency and reconciling continuously.

A good integration is one that fails visibly, recovers without manual intervention and explains every stock change.

If you are facing a similar problem, book a call at https://impact-origin.com/agendamento.

Impact OriginEnjoyed this article?At Impact Origin we help founders and teams build and scale custom software, from a startup MVP to the next step. If you have a project in mind, let's talk.