Skip to content
SystemSyncer
Menu

Guide · Orders

How to Synchronize Shopify Orders with an ERP

How to export Shopify orders to an ERP reliably — choosing the trigger, mapping lines, taxes, discounts and payments, handling edits, and avoiding duplicates.

Last reviewed:
Reading time
6 min read
Intermediate

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.

Data flow Order export workflow
  1. Order placed orders/create or orders/paid
  2. Inbox Deduplicate by webhook ID
  3. Fetch order GraphQL Admin API
  4. Map + validate SKUs
  5. 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 } } }
      }
    }
  }
}
Check field names against your API version

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 totalPriceSet after 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 the updatedAt). 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/updated arriving before orders/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 lineItems connection.

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. [1] orders query (updated_at filter and pagination) — Shopify
  2. [2] Order object — Shopify
  3. [3] Webhook best practices — Shopify
  4. [4] GraphQL Admin API reference (status codes and errors) — Shopify