Short answer
Trigger order export from a webhook (orders/create or orders/paid, depending on when your ERP should see the order), fetch the full order from the GraphQL Admin API, map it to an ERP sales order using the Shopify order ID as an external reference, and write it idempotently. Handle edits, cancellations, and refunds as separate, explicit flows, and run a reconciliation query every few minutes to catch anything missed.
- Order placed orders/create or orders/paid
- Inbox Deduplicate by webhook ID
- Fetch order GraphQL Admin API
- Map + validate SKUs
- ERP sales order Upsert by Shopify ID
Order export is usually the first workflow in any Shopify–ERP integration, and the one where mistakes are most expensive. A missed order is a customer who never receives their goods. A duplicated order is stock shipped twice. This guide covers how to get orders into the ERP once, completely, and with the right values.
Decide when an order belongs in the ERP
Shopify creates an order at checkout, but “created” is not always the moment your ERP should act. Typical choices:
| Trigger | Use when | Trade-off |
|---|---|---|
orders/create |
All orders should be visible in the ERP immediately, including unpaid ones | ERP must handle orders that may never be paid |
orders/paid |
ERP should only receive orders ready to fulfill | Deferred-payment orders arrive later; need a separate view of pending orders |
| Created, then updated on payment | ERP reserves stock for unpaid orders and releases them on payment | More states to model in the ERP |
Write the decision down. It affects inventory, reporting, and customer service, not just the integration.
Also decide which orders to exclude: test orders, orders from specific sales channels, draft orders, or orders containing only digital items or gift cards may not belong in the ERP.
Fetch the full order
Use the webhook as a trigger, then fetch the order from the GraphQL Admin API. The payload might be stale by the time a worker processes it, and the API lets you request exactly the fields your mapping needs.
query OrderExport($id: ID!) {
order(id: $id) {
id
name # e.g. "#1042", useful for humans
createdAt
updatedAt
processedAt
cancelledAt
displayFinancialStatus
displayFulfillmentStatus
currencyCode
email
customer { id }
shippingAddress { name address1 address2 city provinceCode zip countryCodeV2 phone }
billingAddress { name address1 address2 city provinceCode zip countryCodeV2 }
shippingLines(first: 5) {
nodes { title code originalPriceSet { shopMoney { amount } } }
}
taxLines { title rate priceSet { shopMoney { amount } } }
totalDiscountsSet { shopMoney { amount } }
totalPriceSet { shopMoney { amount currencyCode } }
lineItems(first: 100) {
pageInfo { hasNextPage endCursor }
nodes {
id
sku
title
variantTitle
quantity
variant { id }
originalUnitPriceSet { shopMoney { amount } }
discountedTotalSet { shopMoney { amount } }
taxLines { rate priceSet { shopMoney { amount } } }
}
}
}
}
Field names in the GraphQL Admin API occasionally change between versions. Validate this query against the version you pin, and handle hasNextPage on line items for very large orders.
The GraphQL Admin API can return HTTP 200 for requests that failed or were throttled, so always inspect the errors array before using data.
Map the order to the ERP
Mapping is where most order sync bugs live. Work through each area explicitly.
Identity
- Store the Shopify order GID (for example
gid://shopify/Order/1234567890) or the numeric ID as the external reference on the ERP order. Enforce uniqueness on it. - Store the human-readable order name (
#1042) too, for customer service. - Map the shop domain if you run multiple stores into one ERP.
Customer
- B2C stores often post all web orders to a single generic “web customer” account in the ERP and keep addresses on the order. This is simpler and avoids duplicate customer records.
- B2B and account-based stores need a real customer match. Match on a stable key (the Shopify customer ID stored on the ERP customer, or a customer number you assign), not on email alone.
See customer data considerations on the Shopify platform page.
Lines
- Map each line by SKU to an ERP item. If a SKU does not exist in the ERP, fail the job into a review queue — do not silently drop the line. See mapping products to ERP SKUs.
- Bundles and kits need a rule: post the bundle SKU and let the ERP explode it, or post the components.
- Gift cards, tips, and custom items without a SKU need dedicated ERP items or service codes.
Prices, discounts, and taxes
This area causes the most reconciliation differences:
- Decide whether the ERP receives gross or net line prices and whether taxes are recalculated by the ERP or copied from Shopify. Copying is usually safer: the customer paid what Shopify calculated.
- Order-level discounts in Shopify are allocated across lines. Either post the allocated line discounts, or post a separate discount line — but be consistent so revenue reports match.
- Rounding differences of one cent are common when the ERP recalculates. Post a rounding adjustment line rather than letting totals diverge.
- Always compare the ERP order total against Shopify’s
totalPriceSetafter mapping. Reject (into review) any order where they differ by more than your tolerance.
Shipping
Map Shopify shipping line titles or codes to ERP shipping methods and carriers through a configuration table. Shipping rate names change when merchants edit settings; a missing mapping should raise an alert, not fall back silently.
Payments
Map the financial status and payment gateway to ERP payment terms or methods. For paid orders, many ERPs expect a payment or prepayment document alongside the sales order. Decide whether the integration creates it or finance does it separately.
Write idempotently
The ERP write must be safe to retry:
async function exportOrder(order: ShopifyOrder) {
const existing = await erp.findSalesOrderByExternalRef(order.id);
if (existing) {
// Already exported. Record the link in case a previous attempt crashed before doing so.
await links.upsert({ shopifyId: order.id, erpId: existing.number });
return { status: 'already-exported', erpId: existing.number };
}
const draft = mapOrder(order); // pure function, unit-tested
assertTotalsMatch(draft, order); // throws a non-retryable validation error
const created = await erp.createSalesOrder(draft); // ERP enforces unique external_ref
await links.upsert({ shopifyId: order.id, erpId: created.number });
return { status: 'created', erpId: created.number };
}
If the ERP can enforce a unique external reference, a race between two workers produces a constraint error instead of a duplicate. Treat that error as “already exported” and continue. The duplicate-prevention guide covers the remaining edge cases.
Classify errors
Not every failure deserves a retry:
| Error | Example | Action |
|---|---|---|
| Transient | ERP timeout, HTTP 503, Shopify throttled | Retry with backoff |
| Data | Unknown SKU, missing shipping mapping, totals mismatch | Move to review queue, alert operations |
| Duplicate | Unique constraint on external reference | Treat as success, record link |
| Bug | Mapping throws on unexpected null | Dead-letter, alert developers, fix, replay |
Handle edits and cancellations
After export, an order can still change:
- Edits (items added or removed, address changed). Subscribe to
orders/updated, fetch the order, and compare against the version you last exported (store a hash or theupdatedAt). Apply only changes your ERP process allows. - Cancellations (
orders/cancelled). Cancel or credit the ERP order, release reserved stock, and stop fulfillment if it has not started. - Refunds (
refunds/create). These are a financial event, often independent of cancellation. See refunds and cancellations.
Once picking has started in the warehouse, automated edits become risky. A common rule is: before release to the warehouse, sync edits automatically; after release, route them to a person.
Reconcile
Webhooks are not a guarantee. Every few minutes, run a query for orders updated since the last successful checkpoint:
query RecentOrders($cursor: String, $query: String!) {
orders(first: 50, after: $cursor, query: $query, sortKey: UPDATED_AT) {
pageInfo { hasNextPage endCursor }
nodes { id updatedAt }
}
}
# variables: { "query": "updated_at:>'2026-10-26T10:00:00Z'" }
For each order not found in your link table, enqueue an export job. Move the checkpoint forward only after the whole page set is processed, and overlap windows by a few minutes to absorb clock skew.
Security considerations
- Order data contains personal information. Limit who can read the inbox table, and set retention rules for raw payloads.
- Give the integration’s Shopify app only the order and customer scopes it needs.
- Do not log full payment or address details at info level.
Testing checklist
- Order with multiple lines, one with a discount code, one with an automatic discount.
- Order with free shipping and with paid shipping.
- Tax-inclusive and tax-exclusive stores, if you run both.
- Order with a SKU not present in the ERP.
- Same webhook delivered twice;
orders/updatedarriving beforeorders/create. - ERP unavailable for an hour during a burst of orders.
- Order cancelled before and after export.
- Order with more line items than one page of the
lineItemsconnection.
Common mistakes
- Mapping from the webhook payload and never fetching the current order.
- Matching ERP customers on email address only.
- Recalculating tax in the ERP and accepting the difference silently.
- Dropping lines with unknown SKUs instead of failing the order.
- Advancing the reconciliation checkpoint before processing completes.
Summary
Choose the order trigger deliberately, fetch the full order from the API, map identity, lines, prices, taxes, shipping, and payments explicitly, and write to the ERP using the Shopify order ID as a unique external reference. Classify errors so data problems reach people and transient problems retry automatically. Handle edits, cancellations, and refunds as their own flows, and reconcile regularly so no order is left behind.
Frequently asked questions
Should I send orders to the ERP when they are created or when they are paid?
It depends on your payment methods and process. If every order is paid at checkout, creation and payment are nearly simultaneous. If you accept deferred payment methods such as bank transfer or net terms, decide whether the ERP should see unpaid orders (for reservation) or only paid ones (for fulfillment), and choose the trigger accordingly.
Do I need the whole webhook payload?
Not if you fetch the order from the API before processing. Store the payload for audit and debugging, but treat the API response as the source of truth.
How do I handle order edits after the order is in the ERP?
Subscribe to orders/updated (or orders/edited), compare the fetched order against what you last sent, and apply only the changes your ERP process supports. Many teams lock orders in the ERP once picking starts and route later edits to a manual review queue.
How often should order reconciliation run?
Every 5 to 60 minutes is common. The right interval is the longest delay your operations can tolerate for an order that slipped past the webhook path.
Sources and further reading
Platform-specific statements on this page were checked against these sources on . Vendor APIs change; confirm details in the current documentation before you build.
- [1] orders query (updated_at filter and pagination) — Shopify
- [2] Order object — Shopify
- [3] Webhook best practices — Shopify
- [4] GraphQL Admin API reference (status codes and errors) — Shopify
Related reading
- Guides How to Prevent Duplicate Orders in an ERP Integration Why ecommerce integrations create duplicate ERP orders — duplicate webhooks, retries after timeouts, races — and the layered defenses that stop them.
- Guides How to Handle Refunds and Cancellations in an ERP Integration Model ecommerce refunds, cancellations, and returns in an ERP — credit notes, restocking, partial refunds, shipping refunds, and which system initiates each action.
- Guides How to Map Ecommerce Products to ERP Articles and SKUs Design product and variant mapping between an ecommerce platform and an ERP — SKU strategy, variants, bundles, units of measure, cross-references, and validation.
- Guides How to Handle Shopify Webhooks Reliably Build a Shopify webhook receiver that never loses events — HMAC verification, the five-second rule, deduplication, ordering, retries, and subscription removal.
- Guides How to Synchronize Shipment Tracking Between Shopify and an ERP Send shipments and tracking numbers from an ERP or WMS to Shopify using fulfillment orders — partial shipments, multiple packages, carriers, and customer notifications.
- Concepts What Is Data Mapping in Integrations? What data mapping means in ecommerce-ERP integrations — field, value, and structural mapping — with examples, common pitfalls, and how to document and test mappings.