Skip to main content
Article

ERP to ecommerce integration: how to build a link that doesn’t lose orders

· Blog · Web design

Three complaints turn up in almost every conversation we have with a manufacturer about their trade store. The website says a line is in stock and the warehouse says it isn’t. The price on the category page and the price in the basket disagree, and a trade buyer notices. And orders arrive as an email that somebody in the office re-keys into the ERP, which is slow on a good day and wrong on a bad one.

All three are the same problem wearing different clothes. The storefront is a shop window onto systems that already exist and already work. Stock lives in the ERP, prices live in the ERP, and once an order is placed it has to live in the ERP too or nobody picks it. When the window and the warehouse disagree, the integration is where the disagreement started.

This is the part of an ecommerce build with the least room for improvisation. Nothing on the page tells a customer that the stock figure is forty minutes old, or that the price in the basket came from a different calculation than the price on the listing. So the design has to be right before anyone opens an editor. Here is how we approach it, and what we would want you to ask any agency bidding for the work.

Key takeaways

  • Decide ownership before you decide protocol. For every field, one system is the source of truth and the other is a cache. Writing that down is most of the design work.
  • “Real-time” for stock usually means “synchronous”, and synchronous means the ERP’s worst day becomes the storefront’s worst day.
  • An order push must never be able to break a customer’s checkout. Ours records the attempt, lets the customer through, and puts anything unresolved on one screen an operator can act on.
  • Most integration problems are not transport problems. They are type and identity problems: a decimal quantity treated as a whole number, a product with no ERP id, a unit of measure that means one thing on each side.
  • Trade pricing is where the two systems disagree most visibly, because pack sizes, quantity breaks and VAT display all have to agree between catalogue, basket and ERP order line.

Which system owns which field

Start here, not with the API documentation. An integration is a set of agreements about ownership, and the transport is an implementation detail you can change later. Ownership you cannot change later without a data migration.

Everything should be in sync, so “should these be in sync?” is not the useful question for a field. The useful question is “when they disagree, which one is right?” For a manufacturer the answer is almost always the ERP, because that is where the stock count is decremented by a picker and the price is set by whoever sets prices. The exceptions are the fields the ERP has no opinion about.

The ERP is right
  • Free stock, per warehouse
  • Cost and list price
  • Account-level trade terms
  • The order, once placed
  • Despatch and back-order status
  • Product identity: the id every other system has to quote
The storefront is right
  • Category structure and merchandising
  • Images, copy, specification tabs
  • URLs, redirects, canonical tags
  • Search index and facets
  • Web-only promotions and cart rules
  • The basket, until it becomes an order

A typical split for a trade manufacturer. Yours will differ in the middle rows and rarely in the first two.

Two things fall between the columns and cause most of the arguments. Web-only pricing is one: if the storefront can discount, the ERP has to accept a sell price on the order line rather than recalculating it from its own list. Product identity is the other.

We give every storefront product an attribute holding the ERP’s own numeric id, and every order line carries both that id and the storefront SKU. Two identifiers, because SKUs get edited by merchandisers and numeric ids do not. Anything that reaches the ERP without a usable id is caught and held rather than sent and forgotten, which is the subject of the failure-path section below.

“The ERP link” is also usually more than one thing. Orders and customers going one way and catalogue data coming the other are two integrations with two contracts, often with different owners and different schedules. Treating them as one is how a change to one ends up affecting the other.

Why “real-time” is usually the wrong answer

“Real-time stock” is the most commonly requested feature in an ERP integration brief and the one worth pushing back on hardest. What people mean is “accurate”. What it usually buys is a storefront whose availability depends on the ERP being awake.

Ask what the number is actually for. A trade customer buying 200 metres of sleeving needs to know it will ship today. They do not need a figure accurate to the second, because between the page load and the checkout click somebody in the warehouse may have picked the last reel anyway. A stock figure is a promise about despatch, not a live meter, and a promise can be built from a figure that is a few minutes old.

Where synchronous calls genuinely earn their cost is on writes with consequences, and the clearest one is the order push. That one we do run at the moment of order placement, on Magento’s checkout_submit_all_after event, which fires once the order and the payment have both succeeded. We then spend most of the engineering effort on what happens if it doesn’t work.

How we build the order push
0Ways the ERP can stop a customer paying youIf the link is down, the order still completes
1Screen showing every order that hasn’t reached the ERPWith a button that sends it, and a record of what was tried
2Ways each order line names its productSo a renamed product still matches the right one
6Things to agree in writing before anyone writes codeSet out in the table near the end of this page

The four properties we would not ship an ERP integration without.

Here is the part that matters, and it is a Magento fact rather than an ERP one. checkout_submit_all_after is dispatched outside any catch block, so an uncaught error in an observer bound to it takes the customer’s checkout request down with it, after the order row and the payment have already gone through. The customer sees an error. Their card has been charged. The order exists. That is the single worst outcome available in this whole design, and it is entirely preventable.

So we prevent it structurally. Every outbound call has an explicit connect and read timeout rather than whatever the HTTP library defaults to. Every call is inside a catch. The observer as a whole is wrapped so that nothing on the ERP side can reach the person in the checkout: not a slow response, not a network fault, not a malformed body, not an order missing a field the ERP wants. The push can be unsuccessful; the checkout completes either way, and the shortfall is recorded where somebody can act on it.

That is the general rule, and it applies to every synchronous integration point in any system. If a call sits on the critical path of a customer action, it needs a timeout, a catch, and somewhere for the outcome to go. If you cannot give it all three, it should not be on the critical path.

Trade pricing, where the two systems disagree in public

Retail pricing is one number per product. Trade pricing is a small function: a unit price, a pack size, a set of quantity breaks, an account-specific discount, and a VAT display shown both ways because half the audience thinks in ex-VAT and the other half doesn’t. Every one of those is a place the catalogue and the basket can drift apart.

Units of measure are the subtlest of them. A storefront usually models a pack as one line item: quantity 1, of a thing that happens to be 200 metres. An ERP is more likely to model it as a quantity and a unit factor. Those are two different ideas of what “one” means, and the translation between them belongs in explicit, tested mapping rather than in a string comparison somebody wrote in an afternoon. When we build that mapping we make an unrecognised unit stop and report itself rather than pass through with an assumed value.

Types matter for the same reason. In one of our own integrations, quantities were being converted to whole numbers on their way out. We found it ourselves, in code we had written. For anything sold in whole units it is invisible, and it stayed invisible until somebody ordered 2.5 metres of cable and the ERP received an order for 2. We found it by comparing the exact order we had sent against the order that landed, and the fix was a one-word change. That is the characteristic shape of an integration defect: the transport is fine, the authentication is fine, the response says success, and the data is off by an amount only somebody comparing two screens would spot.

Which is the argument for keeping a record of what you actually sent rather than what you meant to send. We store the exact contents of every order attempt, successful or not. Finding that bug took considerably longer than fixing it, and without the log it would have taken longer still.

The display side has its own version of the same problem, and it is the one that costs you trust with a trade buyer. If a price is adjusted in the browser by a VAT toggle, a quantity-break table or a variant swap, it will eventually disagree with the price calculated on the server. The principle is worth stating plainly: calculate once, on the server, and render the same number everywhere. Let the client handle the swap and nothing else.

When the storefront says out of stock and the warehouse disagrees

This is the complaint that gets escalated fastest, and it is worth saying that it is often not the ERP’s fault at all. The ERP can send a perfectly good number into a storefront that then declines to show it.

A concrete example from Magento, because it catches a lot of trade catalogues. Magento’s stock indexer treats a child product with a required custom option as contributing nothing to its parent configurable’s availability. That is defensible in the abstract, because the platform cannot know the option is satisfiable. But a trade catalogue where every variant carries a required pack-size or length option is exactly the shape that trips it, and the result is a parent that aggregates to zero while the shelves are full. The stock feed is not the problem; the query on top of it is. Overriding that indexer behaviour is a contained piece of work once you know that is what you are looking at, and knowing what you are looking at is the expensive part.

The general lesson for a buyer: when stock looks wrong, the diagnosis has to cover the whole path: what the ERP sent, what the import wrote, what the indexer computed, what the cache is serving, and what the template is rendering. An agency that only checks the first of those will tell you the feed is fine, and they will be right and unhelpful at the same time.

Two more general failure classes are worth knowing about, because they are quiet rather than loud, and quiet costs more. The first: incomplete lookup tables. Country codes, currencies, units, tax classes and payment methods all tend to get translated between the two systems through a mapping that was complete on the day it was written. Any mapping with a default branch deserves a periodic audit, because the default is invisible in the data and shows up first in a report nobody expected to be wrong.

The second: HTTP status codes read as business outcomes. A response arriving is not the same as the work having been done, and plenty of well-behaved APIs express a business-level refusal in the body of an otherwise successful response. A client that treats the status line alone as the verdict will record refusals as sends, and those never reach the recovery queue. The mirror case matters just as much. An order the ERP accepted while raising a warning about part of it must not be treated as a failure, because resending it would create a second order. We record those as warnings: visible, and deliberately not resubmittable.

Design the failure path before the happy path

Every integration will be unavailable at some point. The ERP goes down for a version upgrade, a certificate expires, a network path changes. None of that is remarkable. What matters is what the business does on that day, and the answer has to exist in the software rather than in somebody’s head.

Our order push records every attempt, successful or not, with the order reference, the customer, exactly what was sent and what came back. Anything unresolved appears in a single admin screen of orders requiring attention. An operator opens a row, corrects what needs correcting, and resends it as a new attempt. The corrections are usually the ERP product id, the quantity, the unit factor or the net price. The superseded row is kept rather than deleted, so the history of what was tried survives the fixing of it.

There is a detail in that screen worth copying into any build. When an operator supplies an ERP product id to resolve a row, the code writes that id back onto the storefront product. The mapping between the two catalogues is therefore maintained as a by-product of ordinary operational work, and every product that has ever been ordered stays mapped without anybody being assigned to maintain a spreadsheet.

This is not a sophisticated queue, and it does not need to be. What it has is the property that matters most to a small operations team: every order that has not reached the ERP is on one screen, in one place, with a button that sends it. Nobody reconciles two order lists by eye at the end of the day, and nobody discovers a missing order because a customer rang up about it.

Idempotency belongs in the same conversation, because it is cheap to add and painful to retrofit. Every order we send carries a stable reference of our own. If a resend duplicates something, there is a key to find it by. If you send an ERP an order with no reference you control, you have no way to answer “did this one already go?” other than looking.

One more piece of hygiene that costs nothing at build time and a great deal later: the integration has an explicit on/off setting, and it is off on every environment that is not live. Test orders belong in test systems.

The contract to write down before anyone opens an editor

Most integration trouble traces back to something that was obvious to two people in a room at the time and was never written anywhere. Before the first line of code, produce a table like this one and get both sides to sign it, the ERP vendor as well as the agency. If a proposal you are reading does not contain something equivalent, that is a reasonable thing to ask for before you sign it.

FlowDirectionTriggerIf it doesn’t run
Free stockERP → storeScheduledServe the last known figure; alert if the feed is stale
Price and price breaksERP → storeScheduledServe last known; never fall back to zero
New and changed productsERP → storeScheduledSkip the record, log it, do not abort the batch
Customer accountStore → ERPAt order placementRecord and continue; the order still goes
OrderStore → ERPAt order placementNever block checkout; queue for operator resend
Despatch and trackingERP → storeScheduled or on eventOrder stays open; customer email is delayed, not lost
A minimum viable integration contract. Add a column for the field-level owner if both systems allow edits.

Three questions are worth putting to the ERP vendor early, because the answers shape the build and the budget. Does the API accept a sell price on an order line, or does it recalculate from its own price list? Can it return only what has changed since a given time, or does every sync mean reading the whole catalogue? And what are the throughput limits? A nightly full-catalogue read at eight thousand products is a very different proposition from an hourly delta.

A fourth, less technical and more often forgotten: who at the ERP end is empowered to make a change, and what notice do they need? Any integration has a third party in the critical path at some point during a launch. That is a fine thing to discover in the planning and a poor thing to discover on the night.

Where we do this

We build and maintain the integration between Magento 2 and Profit4, OGL Software’s ERP, for a UK trade supplier selling cable management and related products to the trade. Orders and customer accounts flow from the storefront into the ERP; the catalogue side runs on its own schedule.

That link has been in production for years, through catalogue growth, platform upgrades and a full frontend replacement. When we rebuilt that storefront on Hyvä we carried the integration across rather than rewriting it, because it is a backend module bound to a backend event and a theme change has no business touching it. That is not an accident of luck; it is what you get when the integration is built as its own component with its own contract instead of being threaded through the templates.

If your ERP is something else, whether that is Sage, Business Central, SAP, Epicor or a system your predecessor commissioned, the specifics change and the method does not. Ownership, direction, frequency, failure path. Write it down, then build it.

Frequently asked questions

How often should stock sync from an ERP to a storefront?

Often enough that the figure supports the despatch promise you make on the page, which for most trade catalogues is somewhere between every fifteen minutes and hourly. Then add a safety buffer on low-stock lines and handle oversell gracefully at the ERP end, because no sync interval removes the race between two customers.

Should orders push to the ERP in real time or in a batch?

Push immediately, but never inside the request the customer is waiting on unless you have a timeout, a catch and a recovery queue. We push at order placement and wrap the whole thing so the ERP cannot affect checkout. A message queue consumer does the same job with automatic retry built in, and is worth the extra machinery once order volume justifies it.

Can the storefront show customer-specific trade prices from the ERP?

Yes, by two routes. Either import account-level prices into customer groups or tier prices on a schedule, which is fast to render and stale by definition, or call the ERP for a logged-in customer’s price at page load, which is accurate and puts the ERP on the critical path of every product view. Most manufacturers should import.

Why does the catalogue price differ from the basket price?

Almost always because they are calculated in different places. Listing prices rendered or adjusted by JavaScript will eventually drift from a basket calculated server-side, particularly around VAT and quantity breaks. The fix is structural, not cosmetic: calculate once, on the server, and render the same number everywhere.

Does changing the storefront theme break the ERP integration?

It should not, and if it would, that tells you something about how the integration was built. A well-scoped integration is a backend module bound to a backend event, and a frontend replacement leaves it alone. What does need rechecking is anything the theme renders from integration data: stock badges, price breaks, VAT toggles.

How long does an ERP integration take to build?

Less time than the discovery that precedes it, if the discovery is done properly. The build is bounded once ownership, direction, frequency and failure behaviour are agreed for each flow. What extends a project is discovering halfway through that the ERP cannot do something everyone assumed it could, which is why the three vendor questions above are worth asking in week one.

If your storefront and your ERP are arguing

We build and maintain Magento and Hyvä storefronts for UK manufacturers, and most of the difficult work is on this side of the fence rather than in the design. If stock is wrong, prices disagree between the listing and the basket, or orders are being keyed into the ERP by hand, those are all fixable and none of them need a new website. Tell us what your two systems are doing and we will tell you what it would take. Get in touch.