Magento 2 ERP Integration Patterns That Hold Up
Almost every Magento store above a certain size talks to an ERP. Products and prices come from it, stock levels come from it, orders go to it, and invoices and shipments come back. The ERP brand changes from project to project, but the integration problems are the same: who owns which data, how often it moves, what happens when one side is down, and how to avoid duplicate or lost records.
This article goes through the integration patterns I see work in Magento 2.4, where each one fits, and the parts of Magento you use to build them.
Decide data ownership first
Before choosing a transport, write down which system is the source of truth for each entity. A typical split looks like this:
| Entity | Usual owner | Direction |
|---|---|---|
| Product master data, SKUs, cost | ERP | ERP to Magento |
| Marketing content, images, SEO fields | Magento or a PIM | Stays in Magento |
| Prices and tier prices | ERP | ERP to Magento |
| Stock per warehouse | ERP or WMS | ERP to Magento |
| Orders | Magento | Magento to ERP |
| Order status, shipments, tracking | ERP | ERP to Magento |
| Customers | Depends on B2B or B2C | Often both ways |
The point of the table is to forbid edits on the non owner side. If prices come from the ERP, an admin who changes a price in Magento will see it overwritten on the next sync. That is correct behavior, but only if everyone knows it. Some teams lock those fields in the admin form with a small UI component modifier to make the rule visible.
Pattern 1: scheduled batch files
The oldest pattern and still common. The ERP drops CSV or XML files on SFTP, a cron job in Magento picks them up and imports them, and Magento writes order files in the other direction.
It works well for large catalogs that change once or a few times a day, and for ERPs that have no usable API. The weak points are latency and error handling. A file that fails halfway leaves you guessing what was applied. If you go this way:
- Process files in an idempotent way. Importing the same file twice must give the same result.
- Move processed files to an archive folder and failed ones to an error folder, never delete them.
- For products, use the import framework or a bulk approach instead of saving products one by one through the repository.
Pattern 2: the ERP calls Magento APIs
Here the ERP or its connector is the active side. It calls the Magento REST API to update products, prices and stock, and polls for new orders. Magento needs nothing custom for the basic flows, which is the main appeal.
For stock, use the MSI source items endpoint instead of the legacy stock item endpoint, so quantities land on the right source:
curl -X POST "https://example.com/rest/V1/inventory/source-items" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sourceItems":[{"sku":"ABC-123","source_code":"warehouse_sp","quantity":42,"status":1}]}'
For large volumes, the asynchronous bulk endpoints are worth knowing. A request to /rest/async/bulk/V1/products with an array of payloads returns a bulk UUID right away, and the operations are processed by the async.operations.all consumer. You can then check the bulk status through /rest/V1/bulk/{bulkUuid}/status. This keeps the ERP from waiting on hundreds of synchronous product saves.
The weak point of this pattern is order polling. Polling /V1/orders with a filter on updated_at works, but you have to handle clock differences and orders updated during the poll window. Overlap the window a little and deduplicate on the ERP side by increment ID. Use an integration token with only the API resources the ERP needs.
Pattern 3: Magento pushes events through a queue
When orders must reach the ERP within seconds, Magento should push instead of waiting to be polled. Doing the HTTP call to the ERP inside the checkout request is a mistake. If the ERP is slow, checkout is slow. If it is down, you either lose the order export or fail the order. The fix is to publish a message and let a consumer do the call.
Declare a topic in etc/communication.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd">
<topic name="vendor.erp.order.export" request="string"/>
</config>
Publish the order ID from an observer that runs after the order is saved:
<?php
declare(strict_types=1);
namespace Vendor\Erp\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\MessageQueue\PublisherInterface;
use Magento\Framework\Serialize\Serializer\Json;
class PublishOrderExport implements ObserverInterface
{
public function __construct(
private readonly PublisherInterface $publisher,
private readonly Json $json
) {
}
public function execute(Observer $observer): void
{
$order = $observer->getEvent()->getOrder();
if (!$order || !$order->getEntityId()) {
return;
}
$this->publisher->publish(
'vendor.erp.order.export',
$this->json->serialize(['order_id' => (int) $order->getEntityId()])
);
}
}
Then wire queue_publisher.xml, queue_topology.xml and queue_consumer.xml, and write a consumer that loads the order, maps it and sends it. The full wiring, including retries and running consumers under supervisor, is in RabbitMQ consumers in Magento 2.
Send only the ID in the message, not the whole order. The consumer should load fresh data, because the order may have changed between publish and consume. This also keeps messages small and avoids leaking customer data into queue logs.
Pick the event with care. Orders can be created by the storefront, the admin, the REST and GraphQL APIs, and imports, and not every path dispatches the same events. A safety net helps: add an erp_exported_at column on the order and a cron that picks up orders older than a few minutes without that flag. Then a missed event becomes a delay, not a lost order.
Pattern 4: middleware or iPaaS
With several systems around the store (ERP, WMS, marketplaces, CRM), point to point integrations multiply. A middleware layer, whether a commercial iPaaS or a small service you run, sits in the middle, talks to Magento through its APIs and handles mapping, retries and monitoring outside the store.
This keeps Magento lighter and upgrades easier, since less custom code lives in the codebase. The cost is one more system to host and monitor, and the mapping logic is now outside the Magento team's usual tools. It is a good fit when the integration logic is complex or shared between channels. For a store with only an ERP, it is often more than you need.
Things that break every ERP integration
Missing idempotency
Networks fail after the request is processed but before the response arrives. The sender retries, and now there are two shipments or two credit memos. Every write that crosses the boundary should carry an external ID that the receiving side checks before creating anything. In Magento, store the ERP document number on the shipment or invoice in a custom column or extension attribute and look it up before creating a new one. See extension attributes for how to add such fields properly.
Partial updates that wipe data
A product payload without a field can mean "leave it alone" or "clear it", depending on the endpoint and how the connector builds the request. Test this on a staging copy for every field the ERP sends, especially for store view scoped attributes, where a save at the wrong scope can overwrite translations.
Stock races
The ERP sends a full stock snapshot while Magento keeps taking orders. If the snapshot was taken before recent orders were exported, it can raise stock that was already sold. MSI reservations help because salable quantity is calculated as quantity minus open reservations, but the ERP still needs to send quantities that account for orders it already received. The mechanics are in salable quantity and reservations.
No visibility
When a customer asks where their order is, someone has to see if it reached the ERP. Log every export with the order ID, the payload hash, the response and the time. Use a dedicated log file or a database table that support staff can read in the admin. A failed export that nobody sees for three days is the most common real problem in this area.
Choosing a pattern
For most mid size stores, a mix works best: batch or bulk API for catalog and prices, MSI source items API for stock, and queue based push for orders with a cron safety net. Real time for everything is rarely needed and costs a lot in complexity. The order export side, including mapping and status handling, is covered in more detail in exporting Magento 2 orders to an ERP.
FAQ
Should the ERP call Magento or should Magento call the ERP?
Usually the ERP pushes catalog, price and stock data to Magento, and Magento pushes orders out through a queue. The side that owns the data and knows when it changed should start the transfer.
Is it safe to call the ERP API during checkout?
Avoid it. A slow or unavailable ERP then slows down or breaks checkout. Publish a message to a queue and let a consumer send the order in the background.
How do I avoid duplicate orders in the ERP?
Use the Magento increment ID as the external reference and have the ERP reject or update existing records with that reference. On the Magento side, store a flag or the ERP document number once the export succeeds.
What is the async bulk API in Magento?
It is a set of REST endpoints under /rest/async/bulk/ that accept many operations in one request, return a bulk UUID and process them in the background through a message queue consumer.
I am available for freelance Magento and Adobe Commerce work, including ERP integrations. If you need help with this, get in touch.
Related articles
How to Create a Custom REST API Endpoint in Magento 2
Create a custom REST API endpoint in Magento 2.4.8 with webapi.xml, a service interface and ACL. Covers admin, customer and anonymous access with examples.
Magento 2 Extension Attributes: Add Data to Orders and APIs
Add custom data to Magento 2 entities with extension attributes: extension_attributes.xml, repository plugins, storage, REST output and the usual pitfalls.
Magento 2 RabbitMQ Consumers That Don't Die Silently
Set up RabbitMQ message queues in Magento 2.4 the right way: queue XML files, consumers run by cron or supervisor, max-messages and how to spot a dead consumer.
Magento 2 Marketplace Integration: Listings, Orders, Stock
How to connect Magento 2 to marketplaces: connector vs hub vs middleware, category and attribute mapping, importing marketplace orders and avoiding overselling.