---
# === IDENTITY ===
id: business/erp-integration/netsuite-shopify-playbook/2026
canonical_question: "How do you integrate NetSuite with Shopify - the highest-volume SMB integration pattern?"
aliases:
  - "NetSuite Shopify integration playbook"
  - "How to sync Shopify orders to NetSuite"
  - "Shopify NetSuite inventory sync real-time"
  - "Celigo NetSuite Shopify connector setup"
entity_type: erp_integration
domain: business > erp-integration > netsuite-shopify-playbook
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "2025.1 / SuiteTalk REST & SOAP"
    edition: "Standard, Premium, SuiteCloud Plus"
    deployment: cloud
    api_surface: "SuiteTalk SOAP, SuiteTalk REST, RESTlet, SuiteQL"
  - name: "Shopify"
    vendor: "Shopify"
    version: "Admin API 2025-01 (REST + GraphQL)"
    edition: "Basic, Shopify, Advanced, Plus"
    deployment: cloud
    api_surface: "REST Admin API, GraphQL Admin API, Webhooks"
  - name: "Celigo integrator.io"
    vendor: "Celigo"
    version: "Current (SaaS)"
    edition: "Standard, Premium, Enterprise"
    deployment: cloud
    api_surface: "Pre-built Shopify-NetSuite connector"

# === VERIFICATION ===
last_verified: 2026-03-07
confidence: 0.87
version: 1.0
first_published: 2026-03-07

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2024.2 - NetSuite REST API added record-level filtering; Shopify 2024-01 deprecated several REST endpoints in favor of GraphQL"
  next_review: 2026-09-03
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "NetSuite concurrency: 5 concurrent web service requests per account (base); +10 per SuiteCloud Plus license; max 55 at Tier 5"
  - "NetSuite RESTlet per-user concurrency: 5 simultaneous requests"
  - "NetSuite SuiteTalk max: 1,000 records per request/response page"
  - "Shopify REST Admin API: 40 requests per app per store per minute (2/s refill); 10x for Plus stores"
  - "Shopify GraphQL Admin API: 50 points/s (Standard), 100 points/s (Advanced), 500 points/s (Plus); 1,000-point bucket"
  - "Shopify webhook delivery: at-least-once; no guaranteed ordering; 5s timeout for webhook endpoint response"
  - "NetSuite ExternalId must be unique per record type -- cannot reuse across Item subtypes"
  - "Shopify variant SKUs must exactly match NetSuite item names/numbers (case-sensitive, no trailing spaces)"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need NetSuite-only API reference (no Shopify)"
    use_instead: "business/erp-integration/netsuite-suitetalk-api-capabilities/2026"
  - condition: "Need Shopify integration with a different ERP (SAP, Dynamics 365)"
    use_instead: "business/erp-integration/ecommerce-to-erp/2026"
  - condition: "Need Salesforce-to-NetSuite CRM integration (not ecommerce)"
    use_instead: "business/erp-integration/salesforce-netsuite-integration/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (webhook-driven, <1min latency)"
      - "batch/scheduled (every 5-60 minutes)"
      - "event-driven (Shopify webhooks + NetSuite User Event Scripts)"
      - "iPaaS managed (Celigo, Boomi, Workato)"
  - key: data_volume
    question: "What's your daily order volume?"
    type: choice
    options:
      - "< 500 orders/day"
      - "500-5,000 orders/day"
      - "> 5,000 orders/day (Shopify Plus required)"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "orders: Shopify → NetSuite; inventory: NetSuite → Shopify"
      - "bidirectional (products, customers, inventory)"
      - "full lifecycle (orders + fulfillment + returns + refunds)"
  - key: middleware
    question: "Are you using an iPaaS middleware?"
    type: choice
    options:
      - "Celigo (most common for NetSuite-Shopify)"
      - "Boomi"
      - "Workato"
      - "Custom middleware (Node.js, Python)"
      - "NetSuite Connector for Shopify (Oracle native)"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/netsuite-shopify-playbook/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-03-07)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/netsuite-suitetalk-api-capabilities/2026"
      label: "NetSuite SuiteTalk API Capabilities"
    - id: "business/erp-integration/netsuite-tba-vs-oauth2/2026"
      label: "NetSuite TBA vs OAuth 2.0 Authentication"
  related_to:
    - id: "business/erp-integration/netsuite-suitescript-governance/2026"
      label: "NetSuite SuiteScript Governance"
    - id: "business/erp-integration/celigo-netsuite-integration/2026"
      label: "Celigo NetSuite Integration Platform"
    - id: "business/erp-integration/ecommerce-to-erp/2026"
      label: "Ecommerce-to-ERP Integration Patterns"
  solves:
    - id: "business/erp-integration/order-to-cash-integration/2026"
      label: "Order-to-Cash Integration"
    - id: "business/erp-integration/warehouse-management-integration/2026"
      label: "Warehouse Management Integration"
  alternative_to:
    - id: "business/erp-integration/netsuite-restlet-capabilities/2026"
      label: "NetSuite RESTlet Custom Endpoints"
  often_confused_with:
    - id: "business/erp-integration/salesforce-netsuite-integration/2026"
      label: "Salesforce-NetSuite CRM Integration (not ecommerce)"

# === SOURCES ===
sources:
  - id: src1
    title: "NetSuite SuiteTalk REST Web Services API"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_1540391670.html
    type: official_docs
    published: 2025-08-01
    reliability: authoritative
  - id: src2
    title: "Shopify Admin API Rate Limits"
    author: Shopify
    url: https://shopify.dev/docs/api/usage/limits
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src3
    title: "NetSuite Shopify Integration: Complete Technical Guide"
    author: Houseblend
    url: https://www.houseblend.io/articles/netsuite-shopify-integration-technical-guide
    type: technical_blog
    published: 2025-09-15
    reliability: moderate_high
  - id: src4
    title: "Shopify-NetSuite Integration: Complete Step-by-Step Guide"
    author: Anchor Group
    url: https://www.anchorgroup.tech/blog/integrate-shopify-netsuite
    type: technical_blog
    published: 2025-11-20
    reliability: moderate_high
  - id: src5
    title: "Celigo Shopify-NetSuite Integration App Overview"
    author: Celigo
    url: https://docs.celigo.com/hc/en-us/articles/228207227-Shopify-NetSuite-integration-app-overview
    type: official_docs
    published: 2025-06-01
    reliability: high
  - id: src6
    title: "NetSuite API Rate Limits & How to Prevent Hitting Them"
    author: Coefficient
    url: https://coefficient.io/netsuite-api/netsuite-api-rate-limits
    type: technical_blog
    published: 2025-07-10
    reliability: moderate_high
  - id: src7
    title: "Setting Up Real-Time Order Sync for Shopify in NetSuite Connector"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_163723984857.html
    type: official_docs
    published: 2025-04-01
    reliability: authoritative
  - id: src8
    title: "NetSuite Integration Concurrency Limits - 2025"
    author: Katoomi
    url: https://www.katoomi.com/netsuite-integration-concurrency-limits-2025/
    type: technical_blog
    published: 2025-05-12
    reliability: moderate_high
---

# NetSuite-Shopify Integration Playbook

## TL;DR

- **Bottom line**: Shopify handles storefront/checkout; NetSuite is the financial/inventory system of record. Orders flow Shopify → NetSuite as Sales Orders; inventory flows NetSuite → Shopify near-real-time; fulfillment status pushes back to Shopify with tracking numbers. Celigo is the dominant iPaaS for this pair.
- **Key limit**: NetSuite concurrency (5-15 base) is the bottleneck, not Shopify -- every Shopify webhook that triggers a NetSuite write counts against the shared concurrency pool across all integrations. [src1, src6]
- **Watch out for**: SKU mismatches between Shopify variant SKUs and NetSuite item names are the #1 cause of order import failures -- case-sensitive, no trailing spaces, and variant-to-matrix-item mapping is non-trivial. [src3, src4]
- **Best for**: SMB/mid-market DTC brands with 100-10,000 orders/day needing automated order-to-cash with real-time inventory accuracy.
- **Authentication**: NetSuite TBA (Token-Based Authentication) for server-to-server; Shopify OAuth 2.0 or custom app API key for Admin API access. [src1, src2]

## System Profile

This playbook covers the most common SMB ecommerce integration pattern: Shopify as the storefront/checkout platform with Oracle NetSuite as the ERP for financials, inventory, fulfillment, and accounting. It applies to all Shopify plans (Basic through Plus) and all NetSuite editions (Standard through SuiteCloud Plus), though Plus/SuiteCloud Plus unlock higher API limits. The playbook covers three middleware approaches: Celigo (pre-built connector), Oracle's native NetSuite Connector for Shopify, and custom middleware (Node.js/Python). [src3, src5]

| System | Role | API Surface | Direction |
|---|---|---|---|
| Shopify | Storefront, checkout, customer-facing | REST Admin API, GraphQL Admin API, Webhooks | Outbound (orders, customers) + Inbound (inventory, fulfillment) |
| Oracle NetSuite | ERP -- financials, inventory, fulfillment, accounting | SuiteTalk REST, SuiteTalk SOAP, RESTlet, SuiteQL | Inbound (orders) + Outbound (inventory, fulfillment) |
| Celigo / Custom Middleware | Integration orchestrator | Pre-built flows or custom API client | Bidirectional orchestrator |

## API Surfaces & Capabilities

| API Surface | Protocol | Best For | Max Records/Req | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| Shopify REST Admin API | HTTPS/JSON | Orders, products, customers, inventory levels | 250 per page | 40 req/min per app (2/s refill); 400/min Plus | Yes | No |
| Shopify GraphQL Admin API | HTTPS/GraphQL | Complex queries, bulk operations, mutations | Unlimited (paginated) | 50 pts/s (Std), 100 (Adv), 500 (Plus) | Yes | Yes (bulkOperationRunQuery) |
| Shopify Webhooks | HTTPS POST (push) | Real-time event notifications (orders, fulfillments) | 1 event per delivery | At-least-once delivery; 19 retries over 48h | Yes | N/A |
| NetSuite SuiteTalk REST | HTTPS/JSON | CRUD on standard records, SuiteQL queries | 1,000 per page | 5-55 concurrent (account-wide) | Yes | No |
| NetSuite SuiteTalk SOAP | HTTPS/XML | Legacy CRUD, upsertList for batch writes | 1,000 per upsertList | Shares concurrency pool with REST | Yes | Semi |
| NetSuite RESTlet | HTTPS/JSON | Custom endpoints, business logic | Custom (SuiteScript limits) | 5 concurrent per user | Yes | Custom |
| NetSuite SuiteQL | HTTPS/JSON (via REST) | Ad hoc queries, reporting | 100,000 rows per query | Shares concurrency pool | No | Query-only |

[src1, src2, src6]

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max records per REST page | 1,000 | NetSuite SuiteTalk REST | Use offset/limit pagination |
| Max records per SOAP upsertList | 1,000 | NetSuite SuiteTalk SOAP | Split larger batches |
| Max items per Shopify REST page | 250 | Shopify REST Admin API | Use cursor-based pagination |
| Max SuiteQL rows | 100,000 | NetSuite SuiteQL | Paginate with OFFSET |
| Shopify webhook response timeout | 5 seconds | Shopify Webhooks | Return 200 immediately, process async |
| NetSuite payload limit per RESTlet | 10 MB | NetSuite RESTlet values | Chunk larger payloads |

[src1, src2, src6]

### Rolling / Daily Limits

| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| Shopify REST requests | 40 per app per store | Per minute (2/s refill) | Plus: 400/min (20/s refill) |
| Shopify GraphQL cost | 50 points/s (1,000 bucket) | Per second | Advanced: 100 pts/s; Plus: 500 pts/s |
| NetSuite concurrent requests | 5 (base) | Per account, real-time | +10 per SuiteCloud Plus license; Tier 5: 55 |
| NetSuite frequency limit | Account-specific | 60s + 24h windows | Varies by account tier |
| Shopify webhook retries | 19 retries | 48 hours | Exponential backoff by Shopify |

[src1, src2, src6, src8]

### Transaction / Governor Limits

| Limit Type | Per-Transaction Value | Notes |
|---|---|---|
| NetSuite SuiteScript governance units | 1,000 (client), 10,000 (server) | User Event Scripts on order creation consume from this pool |
| NetSuite search results per page | 1,000 | Use pagination for larger result sets |
| NetSuite sublist line limit | ~4,000 lines | Orders with more line items must be split |
| NetSuite concurrent web service users | Account-wide shared pool | One slow integration blocks all others |

[src1, src6]

## Authentication

| Flow | System | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| Token-Based Auth (TBA) | NetSuite | Server-to-server integration (recommended) | Indefinite (until revoked) | No (static tokens) | Consumer key/secret + token ID/secret -- 4 credentials |
| OAuth 2.0 | NetSuite | Modern integrations, granular scopes | Access: 60 min | Yes (refresh token) | Newer; not all SuiteApps support it yet |
| OAuth 2.0 | Shopify | Public apps, app store distribution | Access: varies | Yes | Standard OAuth 2.0 authorization code flow |
| Custom App API Key | Shopify | Private/custom apps (most integrations) | Indefinite | No | Admin API access token generated in Shopify admin |

[src1, src2, src3]

### Authentication Gotchas

- NetSuite TBA requires a dedicated Integration Record in Setup > Integration > Manage Integrations, plus an integration role with minimum required permissions -- never use Administrator role for API access. [src1]
- Shopify custom app tokens are scoped per store -- multi-store integrations need separate tokens per store, each with correct API scopes (read_orders, write_inventory, etc.). [src2]
- NetSuite OAuth 2.0 requires SuiteCloud Plus license and certificate-based authentication for machine-to-machine flow -- not a simple client credentials grant. [src1]
- Shopify webhook HMAC verification is required to prevent spoofed webhook deliveries -- validate the X-Shopify-Hmac-SHA256 header against your app secret. [src2]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- NetSuite concurrency is the system-wide bottleneck: all integrations (Shopify, other apps, custom scripts) share the same 5-55 concurrent request pool. One slow integration degrades all others. [src6, src8]
- Shopify webhooks are at-least-once delivery with no guaranteed ordering -- your middleware must be idempotent (use Shopify order ID as idempotency key). [src2]
- Shopify variant SKUs must exactly match NetSuite item names/numbers. Case-sensitive. Leading/trailing spaces cause silent failures. [src4]
- NetSuite does not natively support Shopify's product variant model -- Shopify variants map to NetSuite Matrix Items + Matrix Sub-Items, which requires a separate item record per variant. [src3]
- Shopify calculates tax at checkout; NetSuite calculates tax independently. If both calculate, amounts will diverge. Choose one tax engine (Avalara/TaxJar) and disable the other's tax calculation. [src4]
- NetSuite Customer records enforce unique email addresses -- Shopify guest checkout creates orders without customer accounts, requiring a merge/dedup strategy. [src4]
- Shopify REST Admin API is being deprecated for new development in favor of GraphQL -- new integrations should use GraphQL where possible, but many iPaaS connectors still rely on REST. [src2]
- Multi-location inventory requires explicit location mapping between Shopify locations and NetSuite warehouses/bins -- unmapped locations cause inventory to sync to the wrong warehouse. [src4]

## Integration Pattern Decision Tree

```
START -- Integrate Shopify storefront with NetSuite ERP
├── What's your order volume?
│   ├── < 500 orders/day
│   │   ├── Budget < $500/month? → NetSuite Connector for Shopify (Oracle native, ~$200/mo)
│   │   └── Need customization? → Celigo Standard ($500-1,000/mo)
│   ├── 500-5,000 orders/day
│   │   ├── Standard flows sufficient? → Celigo Premium ($1,000-2,000/mo)
│   │   └── Complex custom logic? → Custom middleware (Node.js/Python)
│   └── > 5,000 orders/day
│       ├── Requires Shopify Plus (higher API limits)
│       └── Celigo Enterprise or custom high-throughput middleware
├── Which data flows do you need?
│   ├── Orders only (Shopify → NetSuite)
│   │   └── Webhook-driven: orders/create → NetSuite Sales Order
│   ├── Orders + Inventory (bidirectional)
│   │   └── Orders via webhook; inventory via scheduled sync (every 5-15 min)
│   ├── Full lifecycle (orders + fulfillment + returns + refunds)
│   │   └── 5 flows: product sync, inventory sync, order import,
│   │       fulfillment pushback, returns/refunds
│   └── Multi-store?
│       └── Celigo supports cloning store configs; custom needs per-store tokens
├── Inventory sync strategy?
│   ├── Near-real-time (< 5 min lag)
│   │   └── NetSuite Saved Search + scheduled SuiteScript → Shopify inventory_levels/set
│   ├── Batch (every 15-60 min)
│   │   └── Scheduled flow: NetSuite SuiteQL query → bulk update via Shopify GraphQL
│   └── On-demand (on order placement)
│       └── Not recommended -- creates race conditions during flash sales
└── Error handling strategy?
    ├── Zero-loss required → dead letter queue + retry with exponential backoff + alerting
    └── Best-effort → retry 3x, log failures, manual review queue
```

## Quick Reference

### Process Flow Table

| Step | Source | Action | Target | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1. Product Sync | NetSuite | Item created/updated → push product data | Shopify | Item → Product (title, description, price, images, variants/SKUs) | Retry 3x; log SKU mismatches for manual review |
| 2. Inventory Sync | NetSuite | Scheduled query: available quantity per item per location | Shopify | Inventory Item → Inventory Level (quantity_available per location) | Log discrepancies; alert if delta > 10% |
| 3. Order Import | Shopify | Webhook: orders/create fires on new paid order | NetSuite | Order → Sales Order (line items, customer, shipping, tax, payment) | DLQ + retry; create customer if not found; alert on SKU miss |
| 4. Customer Sync | Shopify | New customer or guest checkout → upsert customer | NetSuite | Customer → Customer Record (email, name, address, phone) | Dedup by email; merge if existing; skip if guest-no-email |
| 5. Payment Capture | Shopify | Payment captured → record payment | NetSuite | Transaction → Customer Payment or Cash Sale | Reconcile gateway reference; alert on amount mismatch |
| 6. Fulfillment Pushback | NetSuite | Item Fulfillment created → push tracking to Shopify | Shopify | Item Fulfillment → Fulfillment (tracking_number, tracking_company, line_items) | Retry 3x; alert operations if Shopify 422 |
| 7. Returns/Refunds | Shopify | Refund created → create credit memo | NetSuite | Refund → Credit Memo or Return Authorization (line items, amounts) | Manual review if partial refund; reconcile GL impact |
| 8. Financial Close | NetSuite | Reconcile Shopify payouts against NetSuite deposits | NetSuite | Payout → Bank Deposit (settlement reconciliation) | Compare Shopify Payouts API vs NetSuite deposits; flag variances |

[src3, src4, src5]

## Step-by-Step Integration Guide

### 1. Configure NetSuite Authentication (TBA)

Create a dedicated integration user in NetSuite with minimum required permissions. Never use an admin account for API access. [src1]

```bash
# NetSuite Setup Steps (via UI -- no API for initial setup):
# 1. Setup > Company > Enable Features > SuiteCloud:
#    - Enable Token-Based Authentication
#    - Enable SuiteTalk (Web Services)
#    - Enable REST Web Services
#
# 2. Setup > Integration > Manage Integrations > New:
#    - Name: "Shopify Integration"
#    - Token-Based Authentication: enabled
#    - Record Consumer Key and Consumer Secret
#
# 3. Create integration role (Setup > Users/Roles > Manage Roles):
#    - Permissions needed: Transactions (Sales Order, Item Fulfillment,
#      Credit Memo, Customer Payment), Lists (Items, Customers, Locations),
#      Custom Records (if used)
#
# 4. Create integration employee with this role
# 5. Manage Access Tokens: create token for this employee + integration
#    - Record Token ID and Token Secret
```

**Verify**: Navigate to Setup > Integration > Integration Governance and confirm the new integration appears with 0 active connections.

### 2. Configure Shopify Custom App

Create a custom app in Shopify Admin to generate API credentials. [src2]

```bash
# Shopify Admin > Settings > Apps and sales channels > Develop apps:
# 1. Create new app: "NetSuite Integration"
# 2. Configure Admin API scopes:
#    - read_orders, write_orders
#    - read_products, write_products
#    - read_inventory, write_inventory
#    - read_customers, write_customers
#    - read_fulfillments, write_fulfillments
#    - read_locations
# 3. Install app → generate Admin API access token
# 4. Store token securely (displayed only once)
#
# Webhook subscriptions (Settings > Notifications or via API):
#   - orders/create (fires when order is paid)
#   - orders/updated
#   - refunds/create
#   - customers/create
#   - customers/update
```

**Verify**: `curl -H "X-Shopify-Access-Token: {token}" https://{store}.myshopify.com/admin/api/2025-01/shop.json` returns shop data with 200 status.

### 3. Set Up Order Import (Shopify → NetSuite)

The core flow: Shopify webhook fires on order creation, middleware transforms the payload, NetSuite receives a Sales Order via SuiteTalk REST. [src3, src7]

```javascript
// Node.js middleware: transform Shopify order → NetSuite Sales Order
// Input:  Shopify orders/create webhook payload
// Output: NetSuite Sales Order record via SuiteTalk REST

const crypto = require('crypto');

function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
  const hash = crypto.createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('base64');
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(hmacHeader));
}

function transformOrderToSalesOrder(shopifyOrder) {
  return {
    externalId: `SHOP-${shopifyOrder.id}`,  // Critical: enables idempotent upsert
    entity: { externalId: `SHOPCUST-${shopifyOrder.customer?.id || shopifyOrder.email}` },
    tranDate: shopifyOrder.created_at.split('T')[0],
    memo: `Shopify #${shopifyOrder.order_number}`,
    shipAddress: {
      addr1: shopifyOrder.shipping_address?.address1,
      addr2: shopifyOrder.shipping_address?.address2,
      city: shopifyOrder.shipping_address?.city,
      state: shopifyOrder.shipping_address?.province_code,
      zip: shopifyOrder.shipping_address?.zip,
      country: shopifyOrder.shipping_address?.country_code
    },
    item: {
      items: shopifyOrder.line_items.map(li => ({
        item: { externalId: li.sku },  // Must match NetSuite Item externalId
        quantity: li.quantity,
        rate: parseFloat(li.price),
        amount: parseFloat(li.price) * li.quantity
      }))
    },
    // Custom fields for traceability
    custbody_shopify_order_id: String(shopifyOrder.id),
    custbody_shopify_order_name: shopifyOrder.name
  };
}
```

**Verify**: Create a test order in Shopify development store. Check NetSuite: Transactions > Sales > Sales Orders. Confirm the order appears with correct line items and customer.

### 4. Set Up Inventory Sync (NetSuite → Shopify)

Inventory flows one direction: NetSuite is the source of truth. Query available quantities from NetSuite and push to Shopify inventory levels. [src3, src4]

```javascript
// Node.js: scheduled inventory sync (run every 5-15 minutes)
// Input:  NetSuite available quantities (via SuiteQL)
// Output: Shopify inventory level updates

async function syncInventory(netsuiteClient, shopifyClient) {
  // Step 1: Query NetSuite for current available quantities
  const query = `
    SELECT i.itemId, i.externalId, il.quantityAvailable, il.location
    FROM item i
    JOIN inventoryBalance il ON i.id = il.item
    WHERE i.isInactive = 'F'
    AND il.quantityAvailable IS NOT NULL
  `;
  const items = await netsuiteClient.suiteql(query);

  // Step 2: Build location map (NetSuite location → Shopify location_id)
  const locationMap = {
    '1': '67890123',  // NetSuite "Main Warehouse" → Shopify location
    '2': '67890456',  // NetSuite "East Coast DC" → Shopify location
  };

  // Step 3: Update Shopify inventory levels (batch via GraphQL)
  for (const item of items) {
    const shopifyLocationId = locationMap[item.location];
    if (!shopifyLocationId) continue;  // Skip unmapped locations

    await shopifyClient.post('/admin/api/2025-01/inventory_levels/set.json', {
      location_id: shopifyLocationId,
      inventory_item_id: await lookupShopifyInventoryItemId(item.externalId),
      available: Math.max(0, item.quantityAvailable)  // Never send negative
    });
  }
}
```

**Verify**: Change quantity in NetSuite for a test item. Wait for next sync cycle. Check Shopify Admin > Products > Inventory. Confirm quantity matches.

### 5. Set Up Fulfillment Pushback (NetSuite → Shopify)

When NetSuite creates an Item Fulfillment, push tracking info back to Shopify so customers get shipping notifications. [src3, src5]

```javascript
// Triggered by NetSuite User Event Script (afterSubmit on Item Fulfillment)
// or scheduled middleware polling for new fulfillments

async function pushFulfillmentToShopify(fulfillment, shopifyClient) {
  const shopifyOrderId = fulfillment.custbody_shopify_order_id;
  if (!shopifyOrderId) return;  // Not a Shopify order

  const fulfillmentPayload = {
    fulfillment: {
      tracking_number: fulfillment.trackingNumber,
      tracking_company: mapCarrier(fulfillment.shipMethod),
      line_items: fulfillment.items.map(item => ({
        id: item.shopifyLineItemId,
        quantity: item.quantity
      })),
      notify_customer: true  // Triggers Shopify email notification
    }
  };

  await shopifyClient.post(
    `/admin/api/2025-01/orders/${shopifyOrderId}/fulfillments.json`,
    fulfillmentPayload
  );
}

function mapCarrier(netsuiteShipMethod) {
  const carrierMap = {
    'UPS Ground': 'UPS',
    'FedEx Home Delivery': 'FedEx',
    'USPS Priority': 'USPS',
    'DHL Express': 'DHL Express'
  };
  return carrierMap[netsuiteShipMethod] || 'Other';
}
```

**Verify**: Create a test Item Fulfillment in NetSuite with tracking number. Check Shopify order detail for fulfillment status and tracking URL.

### 6. Handle Returns and Refunds (Shopify → NetSuite)

Refunds created in Shopify must generate Credit Memos (or Return Authorizations) in NetSuite to keep financials in sync. [src3, src5]

```javascript
// Webhook: refunds/create → NetSuite Credit Memo
function transformRefundToCreditMemo(shopifyRefund, originalSalesOrderId) {
  return {
    externalId: `SHOPREFUND-${shopifyRefund.id}`,
    createdFrom: { externalId: `SHOP-${shopifyRefund.order_id}` },
    item: {
      items: shopifyRefund.refund_line_items.map(rli => ({
        item: { externalId: rli.line_item.sku },
        quantity: rli.quantity,
        rate: parseFloat(rli.line_item.price),
        orderLine: rli.line_item.line_number  // Link to original SO line
      }))
    },
    custbody_shopify_refund_id: String(shopifyRefund.id),
    memo: `Shopify Refund for Order #${shopifyRefund.order_id}`
  };
}
```

**Verify**: Process a test refund in Shopify. Check NetSuite: Transactions > Sales > Credit Memos. Confirm amounts and line items match.

## Code Examples

### Python: Bulk Inventory Sync via Shopify GraphQL

```python
# Input:  NetSuite inventory levels (from SuiteQL query)
# Output: Shopify inventory updates via GraphQL bulk mutation

import requests
import time

SHOPIFY_STORE = "your-store.myshopify.com"
SHOPIFY_TOKEN = "shpat_xxxx"
GRAPHQL_URL = f"https://{SHOPIFY_STORE}/admin/api/2025-01/graphql.json"
HEADERS = {
    "X-Shopify-Access-Token": SHOPIFY_TOKEN,
    "Content-Type": "application/json"
}

def bulk_set_inventory(updates: list[dict]):
    """
    updates: [{"inventory_item_id": "gid://...", "location_id": "gid://...", "available": 42}]
    """
    mutation = """
    mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) {
      inventorySetQuantities(input: $input) {
        inventoryAdjustmentGroup { reason }
        userErrors { field message }
      }
    }
    """
    # Shopify GraphQL: 50 pts/s standard, each mutation costs ~10 points
    for i in range(0, len(updates), 5):  # 5 mutations per second = 50 points
        batch = updates[i:i+5]
        for update in batch:
            variables = {
                "input": {
                    "name": "available",
                    "reason": "correction",
                    "quantities": [{
                        "inventoryItemId": update["inventory_item_id"],
                        "locationId": update["location_id"],
                        "quantity": update["available"]
                    }]
                }
            }
            resp = requests.post(GRAPHQL_URL, json={"query": mutation, "variables": variables},
                                 headers=HEADERS)
            if resp.status_code == 429:
                time.sleep(2)  # Back off on rate limit
                continue
            data = resp.json()
            if data.get("data", {}).get("inventorySetQuantities", {}).get("userErrors"):
                print(f"Error: {data['data']['inventorySetQuantities']['userErrors']}")
        time.sleep(1)  # Respect 50 pts/s rate limit
```

### cURL: Test NetSuite SuiteTalk REST Connection

```bash
# Input:  NetSuite TBA credentials (consumer key/secret, token ID/secret)
# Output: Account metadata confirming authentication works

# Test NetSuite REST API authentication
curl -X GET "https://{account_id}.suitetalk.api.netsuite.com/services/rest/record/v1/metadata-catalog" \
  -H "Authorization: OAuth realm=\"{account_id}\",
    oauth_consumer_key=\"{consumer_key}\",
    oauth_token=\"{token_id}\",
    oauth_signature_method=\"HMAC-SHA256\",
    oauth_timestamp=\"$(date +%s)\",
    oauth_nonce=\"$(openssl rand -hex 16)\",
    oauth_version=\"1.0\",
    oauth_signature=\"{computed_signature}\"" \
  -H "Content-Type: application/json"

# Expected: 200 OK with list of available record types

# Test Shopify Admin API authentication
curl -X GET "https://{store}.myshopify.com/admin/api/2025-01/shop.json" \
  -H "X-Shopify-Access-Token: {access_token}"

# Expected: 200 OK with shop details
# Check rate limit headers: X-Shopify-Shop-Api-Call-Limit: 1/40
```

## Data Mapping

### Field Mapping Reference

| Shopify Field | NetSuite Field | Type | Transform | Gotcha |
|---|---|---|---|---|
| order.id | salesOrder.externalId | String | Prefix with "SHOP-" | Must be unique per record type in NetSuite |
| order.order_number | salesOrder.otherRefNum | String | Direct | Human-readable order # (e.g., #1001) |
| order.customer.id | customer.externalId | String | Prefix with "SHOPCUST-" | Guest checkout may not have customer.id |
| order.customer.email | customer.email | Email | Direct | NetSuite enforces unique email per customer record |
| order.line_items[].sku | salesOrder.item[].item.externalId | String | Direct (case-sensitive) | #1 failure cause: SKU mismatch breaks order import |
| order.line_items[].price | salesOrder.item[].rate | Decimal | Direct | Shopify stores in shop currency; NetSuite may require conversion |
| order.line_items[].quantity | salesOrder.item[].quantity | Integer | Direct | Verify positive integer; zero/negative will fail |
| order.shipping_address | salesOrder.shipAddress | Object | Map subfields individually | Shopify province_code vs NetSuite state abbreviation |
| order.total_tax | salesOrder.taxTotal | Decimal | Use Shopify-calculated OR NetSuite-calculated, not both | Dual tax calculation is the #2 integration failure |
| order.financial_status | (determines record type) | String | "paid" → Sales Order; "authorized" → Sales Order with pending payment | Map other statuses (partially_paid, refunded) carefully |
| product.variants[].sku | item.itemId or item.externalId | String | Case-sensitive match | Shopify variant ≠ NetSuite item; use Matrix Items for variants |
| product.variants[].inventory_quantity | N/A (one-way: NS→Shopify) | Integer | NetSuite is source of truth | Never write inventory FROM Shopify TO NetSuite |
| fulfillment.tracking_number | itemFulfillment.trackingNumber | String | Direct | NetSuite may concatenate multiple tracking numbers |
| refund.refund_line_items[].quantity | creditMemo.item[].quantity | Integer | Direct | Partial refund line items require original SO line reference |

[src3, src4, src5]

### Data Type Gotchas

- Shopify stores all monetary amounts as strings (e.g., "29.99"); NetSuite expects numeric types. Always parseFloat() and round to 2 decimal places before writing to NetSuite. [src3]
- Shopify datetime fields are ISO 8601 with timezone (e.g., "2026-03-07T14:30:00-05:00"); NetSuite tranDate is date-only in the company's timezone. Strip the time component and convert timezone. [src3]
- Shopify product tags are a comma-separated string; NetSuite custom lists expect internal IDs. Maintain a lookup table mapping tag strings to NetSuite list values. [src4]
- NetSuite Customer.companyName has an 83-character limit vs Shopify's 255-character company field. Truncate gracefully. [src3]
- Shopify discount_codes are order-level; NetSuite discounts can be line-level or order-level. Decide on a mapping strategy before implementation. [src4]

## Error Handling & Failure Points

### Common Error Codes

| Code | System | Meaning | Cause | Resolution |
|---|---|---|---|---|
| 429 | NetSuite REST | Too Many Requests | Exceeded concurrent request limit | Exponential backoff: wait 2^n seconds (max 60s); reduce concurrency |
| SSS_REQUEST_LIMIT_EXCEEDED | NetSuite SOAP/SuiteScript | Governance limit hit | Too many concurrent requests | Queue requests; implement connection pooling |
| 403 | NetSuite SOAP | Access Denied (rate limit) | SOAP variant of rate limit error | Same as 429 -- backoff and retry |
| INVALID_KEY_OR_REF | NetSuite | Record reference not found | ExternalId doesn't exist in NetSuite | Create referenced record first (customer, item), then retry order |
| RCRD_DSNT_EXIST | NetSuite | Record does not exist | Lookup by internalId/externalId failed | Verify ExternalId prefix convention; check record type |
| 422 | Shopify | Unprocessable Entity | Invalid payload (missing required field, bad value) | Check response body for field-level errors; fix and retry |
| 429 | Shopify | Too Many Requests | Exceeded API rate limit | Wait based on Retry-After header; check X-Shopify-Shop-Api-Call-Limit |
| UNIQUE_CUST_EMAIL | NetSuite | Duplicate customer email | Customer with this email already exists | Lookup existing customer by email; update instead of create |

[src1, src2, src6]

### Failure Points in Production

- **Inventory oversell during flash sales**: Shopify sells faster than NetSuite inventory sync can update. Fix: `Increase sync frequency to every 1-2 minutes during sales events; implement a safety stock buffer (e.g., hold back 5% of NetSuite quantity); use Shopify's inventory reservations API.` [src3]
- **Order import queue backlog at NetSuite concurrency wall**: During peak hours (Black Friday), webhook volume exceeds NetSuite's 5-15 concurrent request limit, causing cascading 429 errors. Fix: `Implement a durable message queue (SQS, Redis) between Shopify webhooks and NetSuite writes; process orders sequentially with controlled concurrency.` [src6, src8]
- **SKU mismatch causing silent order failures**: Orders import without line items because SKU "ABC-123" in Shopify doesn't match "abc-123" in NetSuite (case sensitivity). Fix: `Normalize all SKUs to uppercase in both systems; run a daily reconciliation script comparing Shopify variant SKUs against NetSuite item IDs.` [src4]
- **Guest checkout creating duplicate customers**: Each guest order creates a new customer record because there's no customer ID to match on. Fix: `Always match on email first (NetSuite customer search by email); create customer only if no match; merge duplicates weekly via scheduled script.` [src4]
- **Webhook delivery gaps after Shopify incident**: Shopify webhooks may fail to deliver during platform outages, creating order gaps in NetSuite. Fix: `Run a daily reconciliation job that queries Shopify orders via REST/GraphQL and compares against NetSuite Sales Orders; flag any missing orders for re-import.` [src2, src3]
- **Tax amount mismatch between systems**: Shopify and NetSuite calculate tax differently, causing $0.01-$0.10 discrepancies per order that compound over thousands of orders. Fix: `Use one tax engine (Avalara or TaxJar) for both systems, or pass Shopify's calculated tax to NetSuite and disable NetSuite tax calculation for Shopify orders.` [src4]

## Anti-Patterns

### Wrong: Polling Shopify for new orders

```javascript
// BAD -- polling wastes API calls and introduces latency
// 40 req/min limit means you can only poll every 1.5 seconds
setInterval(async () => {
  const orders = await shopify.get('/admin/api/2025-01/orders.json', {
    status: 'any',
    created_at_min: lastPollTime
  });
  for (const order of orders) {
    await importToNetSuite(order);
  }
}, 30000); // Every 30 seconds
```

### Correct: Use Shopify webhooks for order events

```javascript
// GOOD -- webhook-driven: instant delivery, no wasted API calls
// Register webhook: orders/create
app.post('/webhooks/orders/create', async (req, res) => {
  // 1. Verify HMAC immediately
  if (!verifyShopifyWebhook(req.rawBody, req.headers['x-shopify-hmac-sha256'], SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  // 2. Return 200 within 5 seconds (Shopify timeout)
  res.status(200).send('OK');
  // 3. Process asynchronously (queue)
  await orderQueue.enqueue(req.body);
});
```

### Wrong: Not using ExternalId for idempotent upserts

```javascript
// BAD -- creates duplicate Sales Orders on webhook retry
async function importOrder(shopifyOrder) {
  const salesOrder = transformOrder(shopifyOrder);
  await netsuite.post('/services/rest/record/v1/salesOrder', salesOrder);
  // If this fails after NetSuite creates the record but before
  // you get the response, the webhook retry creates a DUPLICATE
}
```

### Correct: Always use ExternalId with upsert

```javascript
// GOOD -- idempotent: same Shopify order ID always maps to same NetSuite record
async function importOrder(shopifyOrder) {
  const salesOrder = transformOrder(shopifyOrder);
  salesOrder.externalId = `SHOP-${shopifyOrder.id}`;  // Deterministic key
  // PUT with externalId = upsert: create if not exists, update if exists
  await netsuite.put(
    `/services/rest/record/v1/salesOrder/eid:SHOP-${shopifyOrder.id}`,
    salesOrder
  );
  // Safe to retry: duplicate webhooks won't create duplicate orders
}
```

### Wrong: Syncing inventory bidirectionally

```javascript
// BAD -- bidirectional inventory sync causes infinite loops and race conditions
// Shopify sale → update NetSuite → triggers NetSuite→Shopify sync → overwrites Shopify
shopifyWebhook('inventory_levels/update', async (event) => {
  await netsuite.updateInventory(event.inventory_item_id, event.available);
});
netsuiteScheduled(() => {
  const levels = await netsuite.getInventoryLevels();
  await shopify.bulkUpdateInventory(levels);  // Overwrites the update above!
});
```

### Correct: One-way inventory sync (NetSuite → Shopify only)

```javascript
// GOOD -- NetSuite is single source of truth for inventory
// Only sync FROM NetSuite TO Shopify, never the reverse
async function scheduledInventorySync() {
  const levels = await netsuite.suiteql(`
    SELECT itemId, externalId, quantityAvailable, location
    FROM inventoryBalance WHERE lastModifiedDate > ?
  `, [lastSyncTimestamp]);

  for (const item of levels) {
    await shopify.setInventoryLevel(
      item.shopifyInventoryItemId,
      item.shopifyLocationId,
      Math.max(0, item.quantityAvailable)
    );
  }
}
// Shopify orders decrement Shopify inventory automatically;
// NetSuite order import decrements NetSuite inventory;
// Next sync cycle pushes corrected NetSuite quantity to Shopify
```

## Common Pitfalls

- **Multi-store token management**: Each Shopify store requires its own API token, webhook subscriptions, and field mappings. A configuration that works for Store A will silently fail on Store B if SKU conventions or tax settings differ. Fix: `Build a store configuration registry with per-store token, location mapping, and SKU prefix rules; validate on startup.` [src4]
- **NetSuite Saved Search vs SuiteQL for inventory queries**: Saved Searches have an older API, are slower, and have different pagination semantics than SuiteQL. Fix: `Use SuiteQL for all new integrations; it supports standard SQL pagination (OFFSET/FETCH) and returns results faster than Saved Searches.` [src1]
- **Ignoring Shopify's webhook retry behavior**: Shopify retries failed webhooks 19 times over 48 hours with exponential backoff. If your endpoint returns errors during a deployment, you'll get a flood of retries later. Fix: `Always return 200 immediately and process async; implement idempotency using ExternalId; monitor webhook delivery health in Shopify Admin.` [src2]
- **Not accounting for Shopify draft orders**: Draft orders (B2B, phone orders) use a different webhook topic (draft_orders/update) and have a different data structure than regular orders. Fix: `Subscribe to both orders/create and draft_orders/update if you need to capture B2B orders; transform draft order payload separately.` [src2]
- **NetSuite sandbox vs production concurrency mismatch**: NetSuite sandbox accounts often have lower concurrency limits than production, causing tests to pass in dev but fail under production load. Fix: `Load-test with realistic order volumes; request sandbox concurrency matching production from Oracle support before go-live.` [src6, src8]
- **Hardcoding NetSuite internal IDs**: Items, locations, and subsidiaries have internal IDs that differ between sandbox and production. Fix: `Always use ExternalId for all record lookups; never hardcode internal IDs in integration code; maintain an ExternalId registry per environment.` [src1]

## Diagnostic Commands

```bash
# Check Shopify API rate limit usage
curl -s -I -H "X-Shopify-Access-Token: {token}" \
  "https://{store}.myshopify.com/admin/api/2025-01/shop.json" | \
  grep -i "x-shopify-shop-api-call-limit"
# Expected: X-Shopify-Shop-Api-Call-Limit: 1/40

# List Shopify webhook subscriptions
curl -H "X-Shopify-Access-Token: {token}" \
  "https://{store}.myshopify.com/admin/api/2025-01/webhooks.json"
# Verify: orders/create, refunds/create, customers/create are registered

# Check Shopify webhook delivery health
curl -H "X-Shopify-Access-Token: {token}" \
  "https://{store}.myshopify.com/admin/api/2025-01/webhooks/count.json"
# Check Shopify Admin > Settings > Notifications for failed deliveries

# Query NetSuite for recent Shopify orders (via SuiteQL)
# POST to /services/rest/query/v1/suiteql
# Body: {"q": "SELECT id, externalId, tranId, tranDate, status
#   FROM transaction WHERE externalId LIKE 'SHOP-%'
#   ORDER BY tranDate DESC FETCH FIRST 10 ROWS ONLY"}

# Check NetSuite concurrency usage
# Navigate to: Setup > Integration > Integration Governance
# Monitor: Active Sessions, Peak Concurrency, Queue Depth

# Verify NetSuite item exists by ExternalId
# GET /services/rest/record/v1/inventoryItem/eid:{externalId}
# Expected: 200 with item details, or 404 if not found

# Reconcile order counts between systems
# Shopify: GET /admin/api/2025-01/orders/count.json?created_at_min=2026-03-01&status=any
# NetSuite: SuiteQL: SELECT COUNT(*) FROM transaction
#   WHERE externalId LIKE 'SHOP-%' AND tranDate >= '2026-03-01'
# Delta should be 0; any gap indicates missed orders
```

## Version History & Compatibility

| Component | Version | Release Date | Status | Breaking Changes |
|---|---|---|---|---|
| NetSuite 2025.1 | SuiteTalk REST + SOAP | 2025-02 | Current | REST API record-level filtering added |
| NetSuite 2024.2 | SuiteTalk REST + SOAP | 2024-08 | Supported | REST API expanded record type coverage |
| NetSuite 2024.1 | SuiteTalk REST + SOAP | 2024-02 | Supported | REST API added SuiteQL endpoint |
| Shopify Admin API 2025-01 | REST + GraphQL | 2025-01 | Current | Several REST endpoints deprecated (use GraphQL) |
| Shopify Admin API 2024-10 | REST + GraphQL | 2024-10 | Supported | Inventory API v2 (setQuantities mutation) |
| Shopify Admin API 2024-04 | REST + GraphQL | 2024-04 | Supported | Fulfillment Orders API required for fulfillments |
| Celigo Shopify-NetSuite App | SaaS (continuous) | Ongoing | Current | Regular updates; check release notes monthly |

[src1, src2, src7]

### Deprecation Policy

Shopify supports each API version for at least 12 months; deprecated versions return warnings before removal. NetSuite SuiteTalk SOAP is in long-term maintenance mode -- Oracle recommends SuiteTalk REST for all new integrations but has not announced an EOL date for SOAP. [src1, src2]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| SMB/mid-market DTC brand with Shopify storefront + NetSuite back office | Enterprise with 50K+ orders/day needing sub-second sync | Custom event-driven architecture with Kafka/SQS |
| Standard order-to-cash flow (order → fulfill → ship → invoice) | Complex B2B pricing with contract-specific rates managed in NetSuite | NetSuite SuiteCommerce or Shopify B2B with custom pricing rules |
| 1-10 Shopify stores with shared NetSuite instance | Multi-subsidiary NetSuite with intercompany transactions triggered by Shopify orders | Custom middleware with subsidiary routing logic |
| Need turnkey solution with Celigo or Oracle Connector | Need real-time inventory across 50+ warehouse locations with sub-minute accuracy | Warehouse-native integration (WMS → Shopify) bypassing NetSuite for inventory |
| Standard product catalog (< 50K SKUs) | Complex product configurator (BOM/kit assembly driven by Shopify selections) | NetSuite Advanced Manufacturing + custom CPQ integration |

## Important Caveats

- NetSuite concurrency limits are the #1 performance bottleneck in this integration. All integrations (Shopify, other apps, custom scripts, UI users) share the same pool. A slow report can starve the Shopify integration of connections. [src6, src8]
- Shopify is aggressively deprecating REST Admin API endpoints in favor of GraphQL. Many pre-built connectors (including Celigo) still rely on REST internally. Check your connector's API version compatibility before upgrading Shopify API version. [src2]
- Celigo pricing ranges from approximately $500/month (Standard) to $2,000+/month (Enterprise) for the Shopify-NetSuite connector, not including NetSuite SuiteCloud Plus licensing costs for higher concurrency. [src5]
- Tax calculation divergence between Shopify and NetSuite is a compliance risk. Audit monthly by comparing Shopify tax reports against NetSuite tax liability accounts. [src4]
- This playbook covers the standard DTC pattern. B2B wholesale on Shopify (Shopify B2B or Shopify Plus wholesale channel) has a different order structure (draft orders, negotiated pricing, payment terms) that requires additional mapping logic not covered here. [src3]
- Rate limits and concurrency numbers in this card are current as of March 2026. Both NetSuite and Shopify adjust these periodically -- verify against official documentation before capacity planning. [src1, src2]

## Related Units

- [NetSuite SuiteTalk API Capabilities](/business/erp-integration/netsuite-suitetalk-api-capabilities/2026)
- [NetSuite TBA vs OAuth 2.0](/business/erp-integration/netsuite-tba-vs-oauth2/2026)
- [NetSuite SuiteScript Governance](/business/erp-integration/netsuite-suitescript-governance/2026)
- [Celigo NetSuite Integration Platform](/business/erp-integration/celigo-netsuite-integration/2026)
- [Ecommerce-to-ERP Integration Patterns](/business/erp-integration/ecommerce-to-erp/2026)
- [Order-to-Cash Integration](/business/erp-integration/order-to-cash-integration/2026)
- [Salesforce-NetSuite CRM Integration](/business/erp-integration/salesforce-netsuite-integration/2026)
