NetSuite-Shopify Integration Playbook

Type: ERP Integration Systems: Oracle NetSuite (2025.1) + Shopify (Admin API 2025-01) + Celigo Confidence: 0.87 Sources: 8 Verified: 2026-03-07 Freshness: current

TL;DR

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).

SystemRoleAPI SurfaceDirection
ShopifyStorefront, checkout, customer-facingREST Admin API, GraphQL Admin API, WebhooksOutbound (orders, customers) + Inbound (inventory, fulfillment)
Oracle NetSuiteERP — financials, inventory, fulfillment, accountingSuiteTalk REST, SuiteTalk SOAP, RESTlet, SuiteQLInbound (orders) + Outbound (inventory, fulfillment)
Celigo / Custom MiddlewareIntegration orchestratorPre-built flows or custom API clientBidirectional orchestrator

API Surfaces & Capabilities

API SurfaceProtocolBest ForMax Records/ReqRate LimitReal-time?Bulk?
Shopify REST Admin APIHTTPS/JSONOrders, products, customers, inventory250 per page40 req/min (Plus: 400)YesNo
Shopify GraphQL Admin APIHTTPS/GraphQLComplex queries, bulk ops, mutationsPaginated50 pts/s (Plus: 500)YesYes
Shopify WebhooksHTTPS POSTReal-time event notifications1 event/deliveryAt-least-once; 19 retriesYesN/A
NetSuite SuiteTalk RESTHTTPS/JSONCRUD on standard records, SuiteQL1,000 per page5-55 concurrentYesNo
NetSuite SuiteTalk SOAPHTTPS/XMLLegacy CRUD, upsertList batch writes1,000 per upsertListShares concurrency poolYesSemi
NetSuite RESTletHTTPS/JSONCustom endpoints, business logicCustom5 concurrent/userYesCustom
NetSuite SuiteQLHTTPS/JSONAd hoc queries, reporting100,000 rowsShares concurrency poolNoQuery-only

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
Max records per REST page1,000NetSuite SuiteTalk RESTUse offset/limit pagination
Max records per SOAP upsertList1,000NetSuite SuiteTalk SOAPSplit larger batches
Max items per Shopify REST page250Shopify REST Admin APIUse cursor-based pagination
Max SuiteQL rows100,000NetSuite SuiteQLPaginate with OFFSET
Shopify webhook response timeout5 secondsShopify WebhooksReturn 200 immediately, process async
NetSuite payload limit per RESTlet10 MBNetSuite RESTletChunk larger payloads

Rolling / Daily Limits

Limit TypeValueWindowEdition Differences
Shopify REST requests40 per app per storePer minute (2/s refill)Plus: 400/min (20/s refill)
Shopify GraphQL cost50 points/s (1,000 bucket)Per secondAdvanced: 100 pts/s; Plus: 500 pts/s
NetSuite concurrent requests5 (base)Per account, real-time+10 per SuiteCloud Plus license; Tier 5: 55
NetSuite frequency limitAccount-specific60s + 24h windowsVaries by account tier
Shopify webhook retries19 retries48 hoursExponential backoff by Shopify

Transaction / Governor Limits

Limit TypePer-Transaction ValueNotes
NetSuite SuiteScript governance units1,000 (client), 10,000 (server)User Event Scripts on order creation consume from this pool
NetSuite search results per page1,000Use pagination for larger result sets
NetSuite sublist line limit~4,000 linesOrders with more line items must be split
NetSuite concurrent web service usersAccount-wide shared poolOne slow integration blocks all others

Authentication

FlowSystemUse WhenToken LifetimeRefresh?Notes
Token-Based Auth (TBA)NetSuiteServer-to-server (recommended)IndefiniteNo (static)Consumer key/secret + token ID/secret
OAuth 2.0NetSuiteModern integrations, granular scopes60 minYesNewer; not all SuiteApps support it yet
OAuth 2.0ShopifyPublic apps, app store distributionVariesYesStandard OAuth 2.0 authorization code flow
Custom App API KeyShopifyPrivate/custom apps (most integrations)IndefiniteNoAdmin API access token from Shopify admin

Authentication Gotchas

Constraints

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 (~$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?
│   ├── Orders only (Shopify → NetSuite) → Webhook: orders/create
│   ├── Orders + Inventory → Webhooks + scheduled sync (5-15 min)
│   ├── Full lifecycle → 5 flows: product, inventory, order, fulfillment, returns
│   └── Multi-store? → Celigo cloning or per-store tokens
├── Inventory sync strategy?
│   ├── Near-real-time (< 5 min) → Saved Search + scheduled SuiteScript
│   ├── Batch (15-60 min) → SuiteQL + Shopify GraphQL bulk
│   └── On-demand → Not recommended (race conditions)
└── Error handling?
    ├── Zero-loss → DLQ + retry + alerting
    └── Best-effort → Retry 3x + log + manual review

Quick Reference

Process Flow

StepSourceActionTargetData ObjectsFailure Handling
1. Product SyncNetSuiteItem created/updated → pushShopifyItem → Product (title, price, variants, SKUs)Retry 3x; log SKU mismatches
2. Inventory SyncNetSuiteScheduled: available qty per locationShopifyInventory Item → Inventory LevelAlert if delta > 10%
3. Order ImportShopifyWebhook: orders/createNetSuiteOrder → Sales OrderDLQ + retry; create customer if missing
4. Customer SyncShopifyNew customer → upsertNetSuiteCustomer → Customer RecordDedup by email; merge if existing
5. Payment CaptureShopifyPayment captured → recordNetSuiteTransaction → Customer PaymentAlert on amount mismatch
6. Fulfillment PushbackNetSuiteItem Fulfillment → push trackingShopifyFulfillment (tracking, carrier)Retry 3x; alert on 422
7. Returns/RefundsShopifyRefund created → credit memoNetSuiteRefund → Credit Memo / RAManual review if partial
8. Financial CloseNetSuiteReconcile payouts vs depositsNetSuitePayout → Bank DepositFlag variances

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]

# NetSuite Setup (via UI):
# 1. Setup > Company > Enable Features > SuiteCloud:
#    Enable Token-Based Authentication + SuiteTalk + REST Web Services
# 2. Setup > Integration > Manage Integrations > New:
#    Name: "Shopify Integration" / TBA: enabled
#    Record Consumer Key and Consumer Secret
# 3. Create integration role with minimum permissions:
#    Transactions: Sales Order, Item Fulfillment, Credit Memo, Customer Payment
#    Lists: Items, Customers, Locations
# 4. Create integration employee with this role
# 5. Generate Access Token (Token ID + Token Secret)

Verify: Setup > Integration > Integration Governance shows the new integration with 0 active connections.

2. Configure Shopify Custom App

Create a custom app in Shopify Admin for API credentials. [src2]

# Shopify Admin > Settings > Apps > Develop apps > Create app:
# 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
# Install app → generate Admin API access token (shown once)
# Register webhooks: orders/create, 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 200.

3. Set Up Order Import (Shopify → NetSuite)

Shopify webhook fires on order creation; middleware transforms and writes Sales Order to NetSuite via SuiteTalk REST. [src3, src7]

// Transform Shopify order → NetSuite Sales Order
function transformOrderToSalesOrder(shopifyOrder) {
  return {
    externalId: `SHOP-${shopifyOrder.id}`,  // Idempotent upsert key
    entity: { externalId: `SHOPCUST-${shopifyOrder.customer?.id || shopifyOrder.email}` },
    tranDate: shopifyOrder.created_at.split('T')[0],
    memo: `Shopify #${shopifyOrder.order_number}`,
    item: {
      items: shopifyOrder.line_items.map(li => ({
        item: { externalId: li.sku },
        quantity: li.quantity,
        rate: parseFloat(li.price),
        amount: parseFloat(li.price) * li.quantity
      }))
    },
    custbody_shopify_order_id: String(shopifyOrder.id)
  };
}

Verify: Test order in Shopify dev store appears in NetSuite > Transactions > Sales Orders with correct line items.

4. Set Up Inventory Sync (NetSuite → Shopify)

NetSuite is the source of truth. Query available quantities and push to Shopify. Run every 5-15 minutes. [src3, src4]

// Scheduled inventory sync: NetSuite SuiteQL → Shopify inventory_levels/set
async function syncInventory(netsuiteClient, shopifyClient) {
  const items = await netsuiteClient.suiteql(`
    SELECT i.itemId, i.externalId, il.quantityAvailable, il.location
    FROM item i JOIN inventoryBalance il ON i.id = il.item
    WHERE i.isInactive = 'F'`);
  for (const item of items) {
    await shopifyClient.post('/admin/api/2025-01/inventory_levels/set.json', {
      location_id: locationMap[item.location],
      inventory_item_id: await lookupShopifyId(item.externalId),
      available: Math.max(0, item.quantityAvailable)
    });
  }
}

Verify: Change quantity in NetSuite; after sync cycle, Shopify Admin > Products > Inventory matches.

5. Set Up Fulfillment Pushback (NetSuite → Shopify)

When NetSuite creates an Item Fulfillment, push tracking info back to Shopify. [src3, src5]

// Push fulfillment tracking to Shopify
async function pushFulfillment(fulfillment, shopifyClient) {
  await shopifyClient.post(
    `/admin/api/2025-01/orders/${fulfillment.shopifyOrderId}/fulfillments.json`,
    { fulfillment: {
        tracking_number: fulfillment.trackingNumber,
        tracking_company: mapCarrier(fulfillment.shipMethod),
        notify_customer: true
    }}
  );
}

Verify: Test Item Fulfillment in NetSuite; Shopify order shows fulfillment status and tracking URL.

6. Handle Returns and Refunds (Shopify → NetSuite)

Refunds in Shopify generate Credit Memos in NetSuite. [src3, src5]

// Webhook: refunds/create → NetSuite Credit Memo
function transformRefundToCreditMemo(refund) {
  return {
    externalId: `SHOPREFUND-${refund.id}`,
    createdFrom: { externalId: `SHOP-${refund.order_id}` },
    item: { items: refund.refund_line_items.map(rli => ({
      item: { externalId: rli.line_item.sku },
      quantity: rli.quantity,
      rate: parseFloat(rli.line_item.price)
    }))}
  };
}

Verify: Test refund in Shopify; NetSuite Credit Memo appears with matching amounts.

Code Examples

Python: Bulk Inventory Sync via Shopify GraphQL

# Input:  NetSuite inventory levels (from SuiteQL)
# Output: Shopify inventory updates via GraphQL mutation

import requests, time

GRAPHQL_URL = f"https://{STORE}.myshopify.com/admin/api/2025-01/graphql.json"
HEADERS = {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"}

def bulk_set_inventory(updates):
    mutation = """
    mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) {
      inventorySetQuantities(input: $input) {
        inventoryAdjustmentGroup { reason }
        userErrors { field message }
      }
    }"""
    for update in updates:
        variables = {"input": {"name": "available", "reason": "correction",
            "quantities": [{"inventoryItemId": update["iid"],
                "locationId": update["lid"], "quantity": update["qty"]}]}}
        resp = requests.post(GRAPHQL_URL, json={"query": mutation,
            "variables": variables}, headers=HEADERS)
        if resp.status_code == 429:
            time.sleep(2)
        time.sleep(0.2)  # Respect 50 pts/s rate limit

cURL: Test NetSuite and Shopify Connections

# Test Shopify Admin API
curl -H "X-Shopify-Access-Token: {token}" \
  "https://{store}.myshopify.com/admin/api/2025-01/shop.json"
# Check: X-Shopify-Shop-Api-Call-Limit: 1/40

# Test NetSuite REST API (TBA)
curl -X GET \
  "https://{account}.suitetalk.api.netsuite.com/services/rest/record/v1/metadata-catalog" \
  -H "Authorization: OAuth realm=\"{account}\", ..." \
  -H "Content-Type: application/json"
# Expected: 200 OK with record type catalog

Data Mapping

Field Mapping Reference

Shopify FieldNetSuite FieldTypeTransformGotcha
order.idsalesOrder.externalIdStringPrefix "SHOP-"Must be unique per record type
order.order_numbersalesOrder.otherRefNumStringDirectHuman-readable # (e.g., #1001)
order.customer.idcustomer.externalIdStringPrefix "SHOPCUST-"Guest checkout may lack customer.id
order.customer.emailcustomer.emailEmailDirectNS enforces unique email per customer
line_items[].skuitem[].item.externalIdStringCase-sensitive#1 failure cause: SKU mismatch
line_items[].priceitem[].rateDecimalparseFloat()Shopify stores as string
shipping_addressshipAddressObjectMap subfieldsprovince_code vs state abbreviation
total_taxtaxTotalDecimalUse ONE sourceDual tax calculation = #2 failure
variants[].skuitem.itemIdStringCase-sensitiveVariant ≠ Item; use Matrix Items
fulfillment.tracking_numberitemFulfillment.trackingNumberStringDirectNS may concatenate multiple numbers

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeSystemMeaningResolution
429NetSuite RESTToo Many Requests (concurrency exceeded)Exponential backoff: 2^n seconds, max 60s
SSS_REQUEST_LIMIT_EXCEEDEDNetSuite SOAPGovernance limit hitQueue requests; implement connection pooling
INVALID_KEY_OR_REFNetSuiteExternalId reference not foundCreate referenced record first, then retry
UNIQUE_CUST_EMAILNetSuiteDuplicate customer emailLookup existing customer; update not create
422ShopifyUnprocessable Entity (bad payload)Check response body for field-level errors
429ShopifyRate limit exceededWait per Retry-After header

Failure Points in Production

Anti-Patterns

Wrong: Polling Shopify for new orders

// BAD — wastes API calls (40/min limit); introduces 30s+ latency
setInterval(async () => {
  const orders = await shopify.get('/orders.json', {created_at_min: lastPoll});
  for (const order of orders) await importToNetSuite(order);
}, 30000);

Correct: Use Shopify webhooks

// GOOD — instant delivery, no wasted API calls
app.post('/webhooks/orders/create', async (req, res) => {
  if (!verifyHMAC(req)) return res.status(401).send('Invalid');
  res.status(200).send('OK');  // Return within 5s
  await orderQueue.enqueue(req.body);  // Process async
});

Wrong: Not using ExternalId (creates duplicates on retry)

// BAD — webhook retry creates duplicate Sales Orders
await netsuite.post('/salesOrder', transformOrder(shopifyOrder));

Correct: Always use ExternalId with upsert

// GOOD — idempotent: same Shopify order always maps to same record
salesOrder.externalId = `SHOP-${shopifyOrder.id}`;
await netsuite.put(`/salesOrder/eid:SHOP-${shopifyOrder.id}`, salesOrder);

Wrong: Bidirectional inventory sync

// BAD — infinite loops and race conditions
shopifyWebhook('inventory/update', (e) => netsuite.update(e));
netsuiteSchedule(() => shopify.bulkUpdate(netsuite.getLevels()));

Correct: One-way inventory (NetSuite → Shopify only)

// GOOD — NetSuite is single source of truth
async function scheduledSync() {
  const levels = await netsuite.suiteql('SELECT ... FROM inventoryBalance ...');
  for (const item of levels) {
    await shopify.setInventoryLevel(item.id, item.location, item.qty);
  }
}

Common Pitfalls

Diagnostic Commands

# 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"

# NetSuite concurrency monitoring
# 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}

# Reconcile order counts
# Shopify: GET /admin/api/2025-01/orders/count.json?created_at_min=2026-03-01
# NetSuite: SuiteQL: SELECT COUNT(*) FROM transaction
#   WHERE externalId LIKE 'SHOP-%' AND tranDate >= '2026-03-01'

Version History & Compatibility

ComponentVersionRelease DateStatusBreaking Changes
NetSuite 2025.1SuiteTalk REST + SOAP2025-02CurrentREST API record-level filtering
NetSuite 2024.2SuiteTalk REST + SOAP2024-08SupportedREST API expanded record types
Shopify 2025-01REST + GraphQL2025-01CurrentSeveral REST endpoints deprecated
Shopify 2024-10REST + GraphQL2024-10SupportedInventory API v2
Shopify 2024-04REST + GraphQL2024-04SupportedFulfillment Orders API required
Celigo AppSaaS (continuous)OngoingCurrentCheck release notes monthly

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
SMB/mid-market DTC with Shopify + NetSuiteEnterprise 50K+ orders/day needing sub-second syncCustom event-driven architecture (Kafka/SQS)
Standard order-to-cash flowComplex B2B pricing with contract rates in NetSuiteNetSuite SuiteCommerce or Shopify B2B with custom pricing
1-10 Shopify stores, shared NetSuiteMulti-subsidiary with intercompany transactionsCustom middleware with subsidiary routing
Standard catalog (< 50K SKUs)Complex BOM/kit assembly configuratorNetSuite Advanced Manufacturing + custom CPQ

Important Caveats

Related Units