AI Summary - 20-sec read - Reviewed by experts
- If your Shopify-to-Odoo sync occasionally creates the same order two or three times, the integration is not broken at random - it is doing exactly what an unguarded webhook consumer does when the webhook fires more than once.
- Webhooks are delivered at least once, not exactly once. Any timeout, retry, or redelivery means the same order event can arrive again - and if your handler just calls create each time, you get duplicate orders, wrong stock, and double invoices.
- The fix is idempotency: give every event a key, record which keys you have already processed, and skip or update on a repeat. Combined with matching on the Shopify order ID rather than on name or email, a replay finds the existing Odoo order and leaves it alone.
- The same rule protects inventory adjustments, fulfillments, and payments - anywhere a webhook triggers a write. Make every write safe to replay and redelivery stops being a threat.
- Short on time? We will make your Shopify-Odoo sync safe to retry so duplicates stop. Book a free call.
Short on time? Book a free call.
You find it during a stock count, or when a customer emails asking why they were invoiced twice. One order in Shopify has become two or three sale orders in Odoo. Stock is now under-counted because the same sale reserved units more than once, the invoice run has doubled up, and someone has to unpick it by hand. It feels like the integration glitched. It did not. The integration did the one thing an unguarded order sync always eventually does - it processed the same webhook twice - and nothing in the code stopped the second one from creating a whole new order.
Why one order fires the webhook more than once
The root cause surprises people: webhooks are designed to be delivered at least once, not exactly once. Shopify - like every serious webhook sender - guarantees it will keep trying until your endpoint confirms receipt. If your server is slow and does not respond in time, if the network hiccups, or if the confirmation is lost on the way back, Shopify assumes you did not get it and sends the same event again. From its side that is correct behaviour. From your side, the same order-created event has now arrived twice.
If your handler's logic is "on order-created, create a sale order in Odoo", then two deliveries mean two sale orders. There is no bug in the usual sense - each individual run did its job. The flaw is the assumption that each event arrives exactly once, which the delivery model never promised. Any integration built on that assumption will duplicate sooner or later, usually under load, when your server is slow enough to miss the response window and the redeliveries pile up. It is the same discipline that keeps any Odoo API integration stable: assume the call can repeat.
Finding the same order two or three times in Odoo?
We audit your webhook handlers, find every write that is not safe to replay, and make the sync idempotent so duplicates stop for good. No pitch, reply in 2 hrs, no card needed, NDA on request.
Get a free auditThe fix is idempotency: process once, no matter how many times it arrives
An idempotent handler produces the same result whether it runs once or five times. That is exactly the property a webhook consumer needs, because it cannot control how many times an event is delivered. There are two parts to it.
- An idempotency key. Every webhook event carries an identifier - for a Shopify order, the order ID is a natural one. Before you act, record that you are processing this key, and check that record on every arrival. If you have seen the key already, you skip the create. The second and third deliveries become no-ops instead of new orders.
- A dedup check on the write. Even without a formal key store, you can look up whether an Odoo sale order already exists for this Shopify order before creating one. If it exists, update it or do nothing; only create when there is genuinely no match. This is the same safeguard that stops any integration from double-posting.
The mental shift is to treat every webhook as "this might be a repeat" rather than "this is new". Once the handler assumes redelivery is normal, a replayed event is harmless - it finds the work already done and moves on.
Match on the external ID, never on name or email
Idempotency only works if you can reliably recognise the same order on the second arrival, and that means matching on a stable identifier. The Shopify order ID is stable and unique. A customer name is not - two people share names, one person orders twice. An email is not - the same shopper places many orders. Match on the order ID, store it on the Odoo record, and your dedup check is exact.
This is why the classic advice for any sync is to key on the source system's own ID and store it against the target record. Do that and a re-run of the same event finds the existing Odoo order by its Shopify ID and updates in place. Match on anything softer and you either create duplicates or, worse, collide two genuinely different orders into one. It is the same failure mode behind negative inventory from manual returns updates in Shopify - a write that runs when the system already reflects that change.
A retried webhook that calls create is how one order becomes three - and your stock and invoices go wrong.
We make every write in your Shopify-Odoo sync idempotent and keyed on the order ID, so redelivery is harmless. Reply in 2 hrs, NDA on request.
Book a free callTakeaways
- Webhooks are delivered at least once - timeouts and retries mean the same order event can arrive more than once.
- An unguarded handler that calls create on each delivery produces duplicate orders, wrong stock, and double invoices.
- Make the handler idempotent: key each event, record processed keys, and skip or update on a repeat.
- Match on the Shopify order ID, not on name or email, so a replay finds the existing Odoo order exactly.
- Apply the same rule to inventory, fulfillment, and payment webhooks - make every write safe to replay.
Beyond orders: the same rule everywhere a webhook writes
Orders are where duplicates hurt first, but they are not the only place. A fulfillment webhook that runs twice can mark stock as shipped twice. An inventory-update event replayed can adjust a count in the wrong direction. A payment webhook processed again can double a reconciliation. Every one of these is the same problem wearing a different hat - a write that is not safe to replay meeting a delivery model that replays.
So the rule generalises: make every webhook-triggered write idempotent, keyed on the event's stable identifier, and safe to run again. When you do, redelivery stops being a threat and becomes a non-event, which is exactly what you want from a system that syncs money and stock. This is the backbone of the Shopify-Odoo integration and the broader Odoo integration work we do - not clever code, just writes that can be replayed without harm. Getting it right once means you stop firefighting duplicates and start trusting your numbers again.
Frequently asked questions
Can I just tell Shopify to only send each webhook once?
No, and this trips a lot of teams up. At-least-once delivery is a deliberate guarantee, not a setting you can turn off - it is how the sender makes sure you never miss an event. The responsibility for handling repeats sits with the consumer, which is you. The right response is not to fight the delivery model but to make your handler indifferent to how many times an event arrives.
How do I clean up the duplicates already in Odoo?
Carefully, and by matching on the Shopify order ID. Group the Odoo sale orders that share a source order ID, keep the correct one, and cancel or merge the rest while fixing the stock and invoice effects they caused. Do it in a copy of the database first to confirm your matching is right, then apply to live. And fix the handler before you clean up, or you will be de-duplicating again next week.
Does idempotency slow the integration down?
Barely. The extra work is a lookup - check whether this event or this order ID has been handled - before the write. That is a fast indexed read against the cost of creating a wrong record and unpicking it later. In practice an idempotent sync is more reliable and no slower in any way a user would notice.
Is this only a Shopify and Odoo problem?
Not at all. Every webhook-based integration - marketplaces, couriers, payment gateways, any system that pushes events - uses at-least-once delivery and has the same duplicate risk. The pattern is universal: key the event, record what you have processed, make the write safe to replay. Learn it once for Shopify and Odoo and it protects every other integration you build.
The short version: your sync is not glitching at random - it is processing the same webhook twice, because webhooks are delivered at least once and your handler assumes exactly once. Give every event a key, match orders on the Shopify order ID, and make every write safe to replay. Do that and one order stays one order, stock and invoices stay right, and the duplicate cleanup that has been eating your week simply stops.
Leads the Odoo practice at Braincuber. Has delivered Odoo ERP implementations, NetSuite/Tally migrations, and Shopify–Odoo integrations for US mid-market and D2C brands. Owns scoping, data migration, and go-live for every Odoo engagement.
