Notes
Duplicate customers and duplicate orders: where merge errors come from
A duplicate customer appears when the store and the ERP identify the same buyer by different keys: the integration finds no match and creates a new record. A duplicate order appears when a write that already succeeded is sent again, usually after a timeout. Both are prevented the same way: one agreed key per record, checked before anything is created.
Updated
Two systems, two keys
The store and the ERP use a different key for the same customer. Shopify identifies a customer by its own ID and by an email address that is unique in the store: assigning the same address to a second customer returns an error. NetSuite identifies a customer by an internal ID. It does not stop two customers from having the same email address. Its duplicate detection is a feature that has to be enabled, and it flags possible duplicates for a person to merge.
Every order for which the integration cannot connect the two keys produces a new customer in the ERP.
| Situation | What the integration sees | Result without a matching rule |
|---|---|---|
| Guest checkout | No account. In WooCommerce customer_id is 0 |
A new customer for every guest order |
| Known company, new buyer | purchasing@harlow.example in the ERP, dana@harlow.example on the order |
A second customer for the same company |
| One buyer, two companies | The same email, a different company name | The order is booked to the wrong company |
| Different spelling | Harlow Industrial Supply, Inc. and Harlow Industrial Supply Inc |
No match on name |
| The customer exists twice in the ERP | Two hits on the email address | The first hit wins, or the order fails |
| The customer exists in another subsidiary | A hit that cannot be used | The import fails or creates a duplicate |
Standard connectors and iPaaS platforms document the last two rows in their own help pages. Their matching key is the email address. When more than one NetSuite customer has that address the lookup is ambiguous and the import fails, and the advice is to deduplicate the customers in NetSuite before going live.
Work it out for a store with 900 orders a month, 270 of them from guests. Without a match on email that is 270 new customers a month and 3,240 a year, also when they are the same 150 buyers every time. The consequences are in accounting. Open balances are spread over several records, the credit limit per customer is wrong, and a customer price list hangs on the old record while the orders arrive on the new one.
Matching in a fixed order
An integration looks for a customer in a fixed order, from the most reliable key to the least reliable.
| Step | Looks for | Why in this place |
|---|---|---|
| 1 | The stored cross-reference: the store customer ID in the externalId of the ERP record |
Written by the integration, unique, does not change |
| 2 | The email address, normalized | Unique in the store, not in the ERP |
| 3 | Company name and ZIP code, normalized, for B2B orders | Finds a new buyer at a known company. A hit goes to review, not to an automatic match |
| 4 | Nothing found: create the customer and store the cross-reference | Only after steps 1 to 3 return nothing |
For B2B the cross-reference can live on both sides. A company in Shopify has an externalId field, described as a unique externally supplied ID, which holds the NetSuite customer ID.
Four rules make the order reliable.
Normalize before comparing. Dana@Harlow.example with a trailing space and dana@harlow.example are the same address. Harlow Industrial Supply, Inc. and Harlow Industrial Supply Inc are the same company. The integration lowercases, trims and strips punctuation before it searches. The same goes for the ZIP code in step 3, as described in address field mapping.
Two hits is not a match. When a step returns 2 customers, the integration does not pick one. The order goes to a review list with both customer IDs.
Company or individual. NetSuite stores a customer as a company or as an individual, in the field isPerson, and the choice decides which name fields exist. A filled company field on the order makes it a company. Buyers also type N/A, - and none in that field. Without a filter on those values the ERP gets a company called "N/A".
The right subsidiary. In a NetSuite OneWorld account a customer belongs to a subsidiary. A lookup that ignores it finds a record the order cannot use. NetSuite does not merge customers from different subsidiaries afterwards either, so a duplicate made this way stays.
Creating the customer is half of the work. A customer who moves changes the address in the store, and the ERP keeps the old one unless the integration updates it. Per field one system leads: the ERP for payment terms and credit limit, the store for the shipping address of this order. When both sides change the same field within 5 minutes, a rule decides or a person does. That case is worked out in what happens when a sync fails.
Companies, contacts and hierarchy
A B2B store has more than one person per customer. Shopify models a company with locations and contacts. NetSuite has a customer with contacts, and subcustomers that point to a parent through the field parent. Three buyers at Harlow Industrial Supply are three contacts on one customer. Created as three customers they are three open balances and three credit limits for one company.
The hierarchy has a limit. NetSuite allows 10,000 customers and subcustomers in one customer hierarchy, counting every level. A setup that places every web customer under one default parent reaches it. At 270 new customers a month: 10,000 divided by 270 is 37 months. After that NetSuite refuses the next subcustomer, and the order that needed it fails. An iPaaS vendor documents this failure for its own order import.
Duplicate orders
The same mechanism exists on the order side. The integration sends order 31877 to NetSuite. NetSuite creates the sales order, and the connection drops before the response arrives. Oracle's documentation states that a request the server has received is processed even when the client gets no response. The integration does not know whether the order exists and sends it again. Without a check there are two sales orders, two inventory commitments and, when nobody notices, two shipments.
| Cause | The key that catches it |
|---|---|
| Retry after a lost response | External ID SHOP-31877 and an upsert: the second write lands on the same record |
| A webhook delivered twice | X-Shopify-Webhook-Id, stored and checked on arrival |
| A webhook and a scheduled catch-up run pick up the same order | The store order ID as the external ID |
| The same order number from two stores | A prefix per channel: SHOP-1001 and B2B-1001 |
| A PunchOut order resent by the buyer's system | payloadID, and orderID with type="new" |
Take 6,000 orders a month and assume that 1 write in 200 loses its response after NetSuite executed it. That is 6,000 divided by 200, 30 orders a month that are sent a second time. Without a key those are 30 duplicate sales orders. With an external ID they are 30 updates of an order that already exists. For asynchronous requests NetSuite also accepts a header, X-NetSuite-idempotency-key; a second request with the same key is refused with a pointer to the first.
Shopify states that the same webhook can arrive more than once, after a network timeout or a retry. Each delivery carries two IDs:
POST /webhooks/orders-create HTTP/1.1
X-Shopify-Webhook-Id: b54557e4-bdd9-4b37-8a5f-bf7d70bcd043
X-Shopify-Event-Id: 98880550-7158-44d4-b7cd-2c97c8a091b5
The first identifies this delivery, the second the event behind it. Two subscriptions on the same topic produce two deliveries with different webhook IDs and the same event ID. The retry mechanics around these calls are in API throttling, 429s and backoff.
In a PunchOut integration the order comes from the buyer's procurement system as a cXML OrderRequest. When that system gets no answer, it sends the document again; the cXML Reference Guide recommends 10 retries, one hour apart, for a transient error. The header, shortened to the attributes that matter here:
<cXML payloadID="1759497164.4471@buyer.example"
timestamp="2026-10-03T09:12:44-04:00">
<Request>
<OrderRequest>
<OrderRequestHeader orderID="PO-88213" type="new"
orderDate="2026-10-03T09:12:40-04:00"/>
</OrderRequest>
</Request>
</cXML>
The guide says the payloadID should not change for retry attempts, so a second document with the same payloadID is the same order. The receiving side has to make that check. An order that arrives with a new payloadID and the same orderID and type="new" is held for review and not booked a second time.
Duplicates that exist before go-live
The fifth row of the first table is usually older than the integration: a customer entered by hand in 2021 and again in 2024. It goes unnoticed until a lookup returns both.
NetSuite's duplicate detection looks for matches on the email address by default. With more fields selected, records have to match on all of them to be flagged. Merging has limits: two records with different tax registration numbers cannot be merged, and in OneWorld neither can records in different subsidiaries. The cleanup therefore happens in NetSuite, by someone who knows which record is the real one, before orders start to arrive on both.
What this means for your integration
We set the matching order, the rule for two hits and the leading system per field before the integration goes live. In the dry run you see which existing customers share an email address, so they can be merged in the ERP before orders land on them. Every order is written with the store order number as its key, so a retry does not create a second sales order. What cannot be matched to exactly one customer is on a review list in the dashboard.
The order flow as a whole is on orders, customers and shipping, and the combination with NetSuite on Shopify NetSuite integration. Tell us which systems you run and how your customers are set up, and we work out the matching order for your customer base.
Frequently asked questions
How do you prevent duplicate customers in a store to ERP integration?
With a fixed matching order. The integration looks first for a stored cross-reference, such as the store customer ID in the external ID field of the ERP, then for the normalized email address. Only when every step finds nothing does it create a customer. When a step finds two or more records, the order goes to a review list and nothing is created.
Can the integration merge duplicates that already exist in NetSuite?
Merging is done in NetSuite, because both records carry open balances and history. NetSuite restricts it too: records with different tax registration numbers cannot be merged, and in OneWorld neither can records in different subsidiaries. The integration makes the duplicates visible: the dry run before go-live lists the customers that share an email address.
What happens with guest checkouts?
A guest order has no customer account, in WooCommerce customer_id is 0, and it still carries the email address typed at checkout. The integration matches on that address, or books all consumer orders to one generic web customer with the name and address on the order itself. The second option creates no customer records and gives no balance per customer.
Why is the same order in our ERP twice?
Usually because of a retry after a timeout. The ERP had created the order, the response did not arrive and the integration sent the order again. A write that carries the store order number as an external ID lands on the same record the second time, so the retry cannot create a second order.
Should every B2B buyer be a customer in the ERP?
No. The company is the customer, and its buyers are contacts on that customer. Three buyers at one company as three customers splits the open balance and the credit limit over three records. Creating them as subcustomers under one parent runs into a limit: NetSuite allows 10,000 customers and subcustomers in one hierarchy.
Tell us what you need
Tell us which systems you use and what needs to work better. We reply personally within one working day. We help you define the work, then agree the price and timing before you commit.
- You know the price and the timeline before you commit to anything
- We work out the connection that takes the work off your hands
- We handle the build, hosting, monitoring and maintenance
- Your team has less information to retype and fewer discrepancies to chase