How to Sync Shopify Orders to NetSuite
Automatically create NetSuite sales orders from new Shopify orders.
What This Integration Does
The order is captured in Shopify, but every downstream step (fulfilment, invoicing, revenue recognition) runs in NetSuite. Manual re-entry costs hours and creates reconciliation pain at month end. This workflow takes a Shopify order, resolves or creates its customer in NetSuite, then writes a single sales order with one item line per Shopify line item.
It runs via webhook for real-time sync, or schedule for batch catch-up. Each NetSuite sales order carries the Shopify order id as otherrefnum, making the workflow safely re-runnable: duplicate inserts are rejected at the natural-key level.
Prerequisites
- A Shopify connection with
read_ordersandread_customersscopes (plus webhook subscription if using real-time mode). - A NetSuite connection with permission to create customers and sales orders and run SuiteQL.
- An item-mapping strategy: SKU should resolve to a NetSuite item internal id (preload this map in MySQL or MongoDB for speed).
Step 1: Trigger
For real-time, add a Webhook trigger and subscribe Shopify orders/create and orders/paid to its URL. For batch, use a Schedule trigger every 5-10 minutes with a since timestamp.
Step 2: Fetch Order Detail
On the webhook path, the trigger payload already contains the order, but call shopify get-order anyway to guarantee a fresh, complete view (line items, shipping, taxes). On the schedule path, call list-orders filtered by updated_at_min={{ since }} and financial_status=paid, then loop.
Step 3: Resolve or Create the NetSuite Customer
Add a Connector node calling netsuite run-suiteql:
SELECT id FROM customer WHERE email = '{{ order.email }}'
Wrap the result in a Condition node. If no row, call netsuite create-customer with the Shopify billing address. Capture the resulting internal id in nsCustomerId.
Step 4: Transform Line Items
Use a Transform node to map each Shopify line_item to a NetSuite item line. Look up the NetSuite item internal id from your cached SKU map. Carry through quantity and price. Filter out lines where fulfillable_quantity == 0 if your finance team wants only the fulfillable subset.
Step 5: Create the Sales Order
Add a Connector node calling netsuite create-record with type salesOrder:
{
"entity": { "id": "{{ nsCustomerId }}" },
"otherrefnum": "{{ order.name }}",
"trandate": "{{ order.created_at }}",
"memo": "Shopify order {{ order.id }}",
"item": {
"items": [
{ "item": { "id": "{{ line.nsItemId }}" }, "quantity": {{ line.quantity }}, "rate": {{ line.price }} }
]
}
}
Step 6: Confirm and Alert
Add a Condition on the response. On success, optionally call shopify update-order to set a note like NetSuite SO {{ nsSalesOrderId }} for traceability. On failure, route to slack send-message with the Shopify order id and the NetSuite error so a human can investigate without trawling logs.
Tips
- Skip test orders early: a Condition on
order.test == falseat the top of the loop saves NetSuite quota and noise. - Map Shopify
financial_statusto the NetSuite payment-terms picklist so finance sees what they expect. - Set per-node retries with a backoff on the NetSuite step. Webhook bursts during sales often hit concurrency limits and clean up on retry.
Common Pitfalls
- Webhook duplicates - Shopify retries webhooks on non-2xx responses. Rely on
otherrefnumas the dedupe key in NetSuite, and return 200 fast from the webhook trigger. - Discount lines - Shopify line discounts can be at line level or order level. Decide whether to push these as a NetSuite discount line item or to apply them to the rate, but don't double-count.
- Shipping - Shipping in Shopify is its own object. Add it as an explicit shipping line on the NetSuite SO or the totals won't reconcile.
- Currency - For multi-currency stores, set the NetSuite SO currency from
order.currencyexplicitly.
Testing
Place a test order in Shopify (or use the Shopify dev API). Trigger the workflow manually, open the resulting NetSuite sales order in the UI, and verify customer, line items, quantities, rates, and totals match. Re-fire the same webhook payload and confirm the duplicate is rejected cleanly via otherrefnum.