NetSuite-Shopify Integration Playbook
How do you integrate NetSuite with Shopify - the highest-volume SMB integration pattern?
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 — every Shopify webhook that triggers a NetSuite write counts against the shared concurrency pool across all integrations.
- 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.
- 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.
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).
| 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 | 250 per page | 40 req/min (Plus: 400) | Yes | No |
| Shopify GraphQL Admin API | HTTPS/GraphQL | Complex queries, bulk ops, mutations | Paginated | 50 pts/s (Plus: 500) | Yes | Yes |
| Shopify Webhooks | HTTPS POST | Real-time event notifications | 1 event/delivery | At-least-once; 19 retries | Yes | N/A |
| NetSuite SuiteTalk REST | HTTPS/JSON | CRUD on standard records, SuiteQL | 1,000 per page | 5-55 concurrent | Yes | No |
| NetSuite SuiteTalk SOAP | HTTPS/XML | Legacy CRUD, upsertList batch writes | 1,000 per upsertList | Shares concurrency pool | Yes | Semi |
| NetSuite RESTlet | HTTPS/JSON | Custom endpoints, business logic | Custom | 5 concurrent/user | Yes | Custom |
| NetSuite SuiteQL | HTTPS/JSON | Ad hoc queries, reporting | 100,000 rows | Shares concurrency pool | No | Query-only |
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 | Chunk larger payloads |
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 |
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 |
Authentication
| Flow | System | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| Token-Based Auth (TBA) | NetSuite | Server-to-server (recommended) | Indefinite | No (static) | Consumer key/secret + token ID/secret |
| OAuth 2.0 | NetSuite | Modern integrations, granular scopes | 60 min | Yes | Newer; not all SuiteApps support it yet |
| OAuth 2.0 | Shopify | Public apps, app store distribution | 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 from Shopify admin |
Authentication Gotchas
- NetSuite TBA requires a dedicated Integration Record 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 with correct API scopes. [src2]
- NetSuite OAuth 2.0 requires SuiteCloud Plus license and certificate-based authentication for machine-to-machine flow. [src1]
- Shopify webhook HMAC verification is required to prevent spoofed deliveries — validate the X-Shopify-Hmac-SHA256 header against your app secret. [src2]
Constraints
- NetSuite concurrency is the system-wide bottleneck: all integrations share the same 5-55 concurrent request pool. One slow integration degrades all others.
- Shopify webhooks are at-least-once delivery with no guaranteed ordering — middleware must be idempotent (use Shopify order ID as idempotency key).
- Shopify variant SKUs must exactly match NetSuite item names/numbers. Case-sensitive. Leading/trailing spaces cause silent failures.
- NetSuite does not natively support Shopify's product variant model — variants map to Matrix Items + Matrix Sub-Items.
- Tax calculation: choose one engine (Avalara/TaxJar) and disable the other. Dual calculation causes divergence.
- NetSuite Customer records enforce unique email — guest checkout creates orders without accounts, requiring a dedup strategy.
- Shopify REST Admin API is being deprecated for new development in favor of GraphQL.
- Multi-location inventory requires explicit mapping between Shopify locations and NetSuite warehouses.
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
| Step | Source | Action | Target | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1. Product Sync | NetSuite | Item created/updated → push | Shopify | Item → Product (title, price, variants, SKUs) | Retry 3x; log SKU mismatches |
| 2. Inventory Sync | NetSuite | Scheduled: available qty per location | Shopify | Inventory Item → Inventory Level | Alert if delta > 10% |
| 3. Order Import | Shopify | Webhook: orders/create | NetSuite | Order → Sales Order | DLQ + retry; create customer if missing |
| 4. Customer Sync | Shopify | New customer → upsert | NetSuite | Customer → Customer Record | Dedup by email; merge if existing |
| 5. Payment Capture | Shopify | Payment captured → record | NetSuite | Transaction → Customer Payment | Alert on amount mismatch |
| 6. Fulfillment Pushback | NetSuite | Item Fulfillment → push tracking | Shopify | Fulfillment (tracking, carrier) | Retry 3x; alert on 422 |
| 7. Returns/Refunds | Shopify | Refund created → credit memo | NetSuite | Refund → Credit Memo / RA | Manual review if partial |
| 8. Financial Close | NetSuite | Reconcile payouts vs deposits | NetSuite | Payout → Bank Deposit | Flag 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 Field | NetSuite Field | Type | Transform | Gotcha |
|---|---|---|---|---|
| order.id | salesOrder.externalId | String | Prefix "SHOP-" | Must be unique per record type |
| order.order_number | salesOrder.otherRefNum | String | Direct | Human-readable # (e.g., #1001) |
| order.customer.id | customer.externalId | String | Prefix "SHOPCUST-" | Guest checkout may lack customer.id |
| order.customer.email | customer.email | Direct | NS enforces unique email per customer | |
| line_items[].sku | item[].item.externalId | String | Case-sensitive | #1 failure cause: SKU mismatch |
| line_items[].price | item[].rate | Decimal | parseFloat() | Shopify stores as string |
| shipping_address | shipAddress | Object | Map subfields | province_code vs state abbreviation |
| total_tax | taxTotal | Decimal | Use ONE source | Dual tax calculation = #2 failure |
| variants[].sku | item.itemId | String | Case-sensitive | Variant ≠ Item; use Matrix Items |
| fulfillment.tracking_number | itemFulfillment.trackingNumber | String | Direct | NS may concatenate multiple numbers |
Data Type Gotchas
- Shopify stores monetary amounts as strings (e.g., "29.99"); NetSuite expects numeric. Always parseFloat() and round to 2 decimals. [src3]
- Shopify datetime is ISO 8601 with timezone; NetSuite tranDate is date-only in company timezone. Strip time and convert. [src3]
- NetSuite Customer.companyName has 83-char limit vs Shopify's 255. Truncate gracefully. [src3]
- Shopify discount_codes are order-level; NetSuite discounts can be line-level or order-level. Decide mapping strategy before implementation. [src4]
Error Handling & Failure Points
Common Error Codes
| Code | System | Meaning | Resolution |
|---|---|---|---|
| 429 | NetSuite REST | Too Many Requests (concurrency exceeded) | Exponential backoff: 2^n seconds, max 60s |
| SSS_REQUEST_LIMIT_EXCEEDED | NetSuite SOAP | Governance limit hit | Queue requests; implement connection pooling |
| INVALID_KEY_OR_REF | NetSuite | ExternalId reference not found | Create referenced record first, then retry |
| UNIQUE_CUST_EMAIL | NetSuite | Duplicate customer email | Lookup existing customer; update not create |
| 422 | Shopify | Unprocessable Entity (bad payload) | Check response body for field-level errors |
| 429 | Shopify | Rate limit exceeded | Wait per Retry-After header |
Failure Points in Production
- Inventory oversell during flash sales: Shopify sells faster than sync updates. Fix:
Increase sync frequency during sales; implement safety stock buffer; use Shopify inventory reservations. [src3] - Order queue backlog at NetSuite concurrency wall: Peak webhook volume exceeds 5-15 concurrent limit, causing cascading 429s. Fix:
Durable message queue (SQS/Redis) between webhooks and NetSuite; controlled concurrency. [src6, src8] - SKU mismatch causing silent order failures: "ABC-123" ≠ "abc-123" (case sensitivity). Fix:
Normalize all SKUs to uppercase; daily reconciliation script. [src4] - Guest checkout creating duplicate customers: No customer ID to match on. Fix:
Match on email first; create only if no match; weekly dedup. [src4] - Webhook delivery gaps after Shopify outage: Orders missing in NetSuite. Fix:
Daily reconciliation job comparing Shopify orders vs NetSuite Sales Orders. [src2] - Tax amount mismatch: Divergent calculations compound over thousands of orders. Fix:
Use one tax engine for both systems; disable the other's calculation. [src4]
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
- Multi-store token management: Each Shopify store needs its own API token, webhooks, and field mappings. Fix:
Store configuration registry with per-store token, location map, and SKU prefix. [src4] - Saved Search vs SuiteQL: Saved Searches are slower with older pagination. Fix:
Use SuiteQL for all new integrations; standard SQL pagination (OFFSET/FETCH). [src1] - Ignoring Shopify webhook retry behavior: 19 retries over 48h; deployment outage causes retry flood. Fix:
Return 200 immediately; process async; implement idempotency via ExternalId. [src2] - Ignoring draft orders: B2B/phone orders use different webhook topic and data structure. Fix:
Subscribe to draft_orders/update; transform separately. [src2] - Sandbox concurrency mismatch: NetSuite sandbox has lower limits than production. Fix:
Load-test with realistic volumes; request matching concurrency from Oracle. [src6] - Hardcoding NetSuite internal IDs: IDs differ between sandbox and production. Fix:
Use ExternalId for all lookups; maintain registry per environment. [src1]
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
| Component | Version | Release Date | Status | Breaking Changes |
|---|---|---|---|---|
| NetSuite 2025.1 | SuiteTalk REST + SOAP | 2025-02 | Current | REST API record-level filtering |
| NetSuite 2024.2 | SuiteTalk REST + SOAP | 2024-08 | Supported | REST API expanded record types |
| Shopify 2025-01 | REST + GraphQL | 2025-01 | Current | Several REST endpoints deprecated |
| Shopify 2024-10 | REST + GraphQL | 2024-10 | Supported | Inventory API v2 |
| Shopify 2024-04 | REST + GraphQL | 2024-04 | Supported | Fulfillment Orders API required |
| Celigo App | SaaS (continuous) | Ongoing | Current | Check release notes monthly |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| SMB/mid-market DTC with Shopify + NetSuite | Enterprise 50K+ orders/day needing sub-second sync | Custom event-driven architecture (Kafka/SQS) |
| Standard order-to-cash flow | Complex B2B pricing with contract rates in NetSuite | NetSuite SuiteCommerce or Shopify B2B with custom pricing |
| 1-10 Shopify stores, shared NetSuite | Multi-subsidiary with intercompany transactions | Custom middleware with subsidiary routing |
| Standard catalog (< 50K SKUs) | Complex BOM/kit assembly configurator | NetSuite Advanced Manufacturing + custom CPQ |
Important Caveats
- NetSuite concurrency limits are the #1 performance bottleneck. All integrations share the same pool — a slow report can starve the Shopify integration of connections.
- Shopify is aggressively deprecating REST Admin API endpoints in favor of GraphQL. Check connector API version compatibility before upgrading.
- Celigo pricing ranges from ~$500/month (Standard) to $2,000+/month (Enterprise), not including NetSuite SuiteCloud Plus licensing.
- Tax calculation divergence is a compliance risk. Audit monthly by comparing Shopify tax reports against NetSuite tax liability accounts.
- This playbook covers standard DTC. Shopify B2B wholesale has different order structure (draft orders, negotiated pricing) requiring additional mapping logic.
- Rate limits and concurrency numbers are current as of March 2026. Both platforms adjust periodically — verify against official docs before capacity planning.