A duplicate order in the ERP is not a harmless glitch: it creates an extra delivery note, throws stock off and ends in an awkward call with the customer. It almost always comes down to the same thing: the integration assumes every message arrives exactly once. This is how to design an online store–ERP integration that survives retries, outages and errors without duplicating anything.
Why orders get duplicated
- Automatic retries: the store or webhook resends the order because the ERP was slow to reply, even though it had saved it.
- Multiple triggers: the order is sent on creation and again on payment, or from a cron and an event at the same time.
- Parallel processes: two workers pick up the same order simultaneously.
- Manual resends: someone clicks "send again" without knowing whether the first attempt arrived.
1. Decide which system owns each piece of data
Before writing code, decide the source of truth: usually the store owns orders and web customers, and the ERP owns stock, prices and shipping status. Each piece of data travels in one direction only. "Sync everything both ways" integrations are the ones that create most duplicates and conflicts.
2. Use an idempotency key
Every order needs a unique, stable identifier, such as the store’s order number. The receiver stores it and, if it arrives again, returns the previous result instead of creating another order. With a UNIQUE constraint in the database, not even two simultaneous processes can get around it:
// Control table: integration_orders (external_id UNIQUE, erp_id, status)
$record = IntegrationOrder::firstOrCreate(
['external_id' => $order->number],
['status' => 'pending']
);
if ($record->erp_id) {
return $record->erp_id; // already sent: no duplicate
}
$erpId = $erp->createOrder($order);
$record->update(['erp_id' => $erpId, 'status' => 'sent']);
3. Queue the sends and retry sensibly
Sending to the ERP should not happen inside the customer’s request. Put it on a queue, with limited retries and increasing back-off. And separate errors: a 400 (bad data) is not fixed by retrying; a 503 or a timeout is.
4. Validate the format before sending
Many rejections come from format mismatches: an address sent as an object when the ERP expects flat fields, decimals with commas, different country codes. Document the data contract and validate the order before sending it. That is exactly what we solved in the Magento 2 order integration with an external API case.
5. Log everything with context
Every send should leave a trail: order, payload, response, status code and attempt number. When something fails, the alert has to say which order and why, not just "integration error".
6. Have a reconciliation process
Even with a good design, a daily check pays off: store orders without an ERP ID, ERP orders without a store origin and statuses that do not match. Catching it the same day is far cheaper than finding it at month-end.
Quick checklist
- Each piece of data has a single owning system.
- Each order carries an idempotency key and there is a unique constraint at the destination.
- Sends go through a queue, with retries only for temporary errors.
- The format is validated before sending.
- There are contextual logs and useful alerts.
- There is periodic reconciliation.
If your store and your ERP do not quite get along, I can review the integration and propose a phased plan. See my development and integration services or tell me about your case.