---
# === IDENTITY ===
id: business/erp-integration/salesforce-netsuite-integration/2026
canonical_question: "How do you integrate Salesforce and NetSuite - O2C flow, field mapping, auth, common failures?"
aliases:
  - "Salesforce to NetSuite order-to-cash integration"
  - "How to sync Salesforce orders to NetSuite sales orders"
  - "Salesforce NetSuite field mapping and authentication"
  - "Celigo Boomi Salesforce NetSuite integration best practices"
entity_type: erp_integration
domain: business > erp-integration > salesforce-netsuite-integration
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Salesforce"
    vendor: "Salesforce"
    version: "API v62.0 (Spring '26)"
    edition: "Enterprise, Unlimited, Developer"
    deployment: cloud
    api_surface: "REST, SOAP, Bulk API 2.0, Composite, Streaming"
  - 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"

# === VERIFICATION ===
last_verified: 2026-03-02
confidence: 0.88
version: 1.0
first_published: 2026-03-02

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2024.2 - Salesforce removed daily API call cap; NetSuite 2024.1 added REST API record filtering"
  next_review: 2026-08-29
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "NetSuite concurrency: 5 concurrent web service requests per account (base); 15 with SuiteCloud Plus"
  - "NetSuite RESTlet per-user concurrency: 5 simultaneous requests"
  - "NetSuite SuiteTalk SOAP upsertList max: 1,000 records per request"
  - "NetSuite REST API max: 1,000 records per response page"
  - "Salesforce Composite API max: 25 subrequests per call"
  - "Salesforce Apex callout timeout: 120 seconds max per callout"
  - "Salesforce governor limits: 100 SOQL queries, 150 DML statements, 100 callouts per transaction"
  - "NetSuite Customer and Employee share internal/external IDs -- must be unique across both entity types"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need Salesforce-only API reference (no NetSuite)"
    use_instead: "business/erp-integration/salesforce-rest-api-capabilities/2026"
  - condition: "Need NetSuite-only API reference (no Salesforce)"
    use_instead: "business/erp-integration/netsuite-suitetalk-rest-capabilities/2026"
  - condition: "Need Salesforce-to-Dynamics 365 integration"
    use_instead: "business/erp-integration/salesforce-dynamics365-integration/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to NetSuite)"
      - "outbound (reading from NetSuite)"
      - "bidirectional sync"
  - key: middleware
    question: "Are you using an iPaaS middleware?"
    type: choice
    options:
      - "Celigo"
      - "Boomi"
      - "MuleSoft"
      - "Workato"
      - "None (custom code)"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/salesforce-netsuite-integration/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-03-02)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/salesforce-rest-api-capabilities/2026"
      label: "Salesforce REST API capabilities and rate limits"
    - id: "business/erp-integration/netsuite-suitetalk-rest-capabilities/2026"
      label: "NetSuite SuiteTalk REST API capabilities"
  related_to:
    - id: "business/erp-integration/netsuite-tba-vs-oauth2/2026"
      label: "NetSuite TBA vs OAuth 2.0 authentication comparison"
    - id: "business/erp-integration/netsuite-suitescript-governance/2026"
      label: "NetSuite SuiteScript governance limits"
  solves:
    - id: "business/erp-integration/salesforce-netsuite-integration/2026"
      label: "End-to-end O2C integration playbook"
  alternative_to:
    - id: "business/erp-integration/salesforce-dynamics365-integration/2026"
      label: "Salesforce to Dynamics 365 integration (alternative ERP)"
  often_confused_with:
    - id: "business/erp-integration/netsuite-restlet-capabilities/2026"
      label: "NetSuite RESTlets (custom endpoints, not standard SuiteTalk REST)"

# === SOURCES ===
sources:
  - id: src1
    title: "Salesforce REST API Developer Guide - Limits"
    author: Salesforce
    url: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_limits.htm
    type: official_docs
    published: 2026-02-01
    reliability: authoritative
  - id: src2
    title: "NetSuite SuiteTalk REST API - External IDs Overview"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N3433806.html
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src3
    title: "A Technical Guide to Salesforce-NetSuite Order Integration"
    author: Houseblend
    url: https://www.houseblend.io/articles/salesforce-netsuite-order-integration-guide
    type: technical_blog
    published: 2025-10-01
    reliability: high
  - id: src4
    title: "Sync Salesforce Order to NetSuite Sales Order - Celigo Help Center"
    author: Celigo
    url: https://docs.celigo.com/hc/en-us/articles/360035951132-Sync-Salesforce-Order-to-NetSuite-Sales-Order
    type: official_docs
    published: 2025-08-01
    reliability: high
  - id: src5
    title: "NetSuite Integration Concurrency Limits 2025"
    author: Katoomi
    url: https://www.katoomi.com/netsuite-integration-concurrency-limits-2025/
    type: technical_blog
    published: 2025-04-01
    reliability: moderate_high
  - id: src6
    title: "NetSuite-Salesforce Integration: Tools, Costs & Fixing Common Failures"
    author: Stockton10
    url: https://www.stockton10.com/blog/netsuite-salesforce-integration-tools-costs-fixing-common-failures
    type: technical_blog
    published: 2025-09-01
    reliability: moderate_high
  - id: src7
    title: "Salesforce-NetSuite Integration Guide"
    author: Workato
    url: https://www.workato.com/the-connector/salesforce-netsuite-integration-guide/
    type: technical_blog
    published: 2025-07-01
    reliability: moderate_high
---

# Salesforce-NetSuite Integration: O2C Flow, Field Mapping, Auth & Common Failures

## TL;DR

- **Bottom line**: Use ExternalID-based upserts for all cross-system record sync to prevent duplicates; Celigo or Boomi for out-of-box O2C flows, custom RESTlet/REST API for complex logic.
- **Key limit**: NetSuite concurrency cap -- 5 concurrent web service requests (base account), 15 with SuiteCloud Plus -- shared across ALL integrations (SOAP + REST + RESTlet).
- **Watch out for**: Salesforce Account is NOT equal to a NetSuite Customer -- field-level mapping mismatches (name length, address format, picklist values) cause silent data corruption in production.
- **Best for**: Order-to-cash automation where Salesforce owns the sales pipeline and NetSuite owns fulfillment, invoicing, and financial close.
- **Authentication**: Salesforce OAuth 2.0 JWT Bearer (server-to-server) + NetSuite Token-Based Authentication (TBA) using OAuth 1.0a -- NLAuth deprecated since 2020.

## System Profile

This integration playbook covers the bidirectional data flow between Salesforce CRM and Oracle NetSuite ERP for the order-to-cash (O2C) process. Salesforce serves as the system of record for leads, opportunities, and customer relationships. NetSuite serves as the system of record for sales orders, fulfillment, invoices, and payments. The integration synchronizes accounts/customers, products/items, orders, fulfillment status, and financial documents between the two systems.

This card does NOT cover: Salesforce CPQ-specific integration patterns, NetSuite SuiteBilling integration, or multi-subsidiary consolidation scenarios. For those, look for specialized playbook cards.

| System | Role | API Surface | Direction |
|---|---|---|---|
| Salesforce (API v62.0) | CRM -- source of truth for customers, opportunities, quotes | REST API, Composite API, Bulk API 2.0 | Outbound (customer/order data) + Inbound (fulfillment/invoice sync-back) |
| Oracle NetSuite (2025.1) | ERP -- financial master for orders, fulfillment, invoicing | SuiteTalk SOAP, SuiteTalk REST, RESTlets, SuiteQL | Inbound (orders from SF) + Outbound (fulfillment/invoice to SF) |
| Middleware (iPaaS) | Integration orchestrator | Celigo / Boomi / MuleSoft / Workato | Bidirectional orchestration |

## API Surfaces & Capabilities

| API Surface | System | Protocol | Best For | Max Records/Request | Concurrency | Real-time? | Bulk? |
|---|---|---|---|---|---|---|---|
| REST API v62.0 | Salesforce | HTTPS/JSON | Individual record CRUD, queries | 2,000 per SOQL page | 25 concurrent long-running | Yes | No |
| Composite API | Salesforce | HTTPS/JSON | Multi-object operations in single call | 25 subrequests | Shared with REST | Yes | No |
| Bulk API 2.0 | Salesforce | HTTPS/CSV | ETL, data migration, >2K records | 150 MB per file | 15,000 batches/24h | No | Yes |
| Platform Events / CDC | Salesforce | Bayeux/CometD | Real-time notifications, change tracking | N/A | Edition-dependent | Yes | N/A |
| SuiteTalk SOAP | NetSuite | HTTPS/XML | Full record CRUD, upsert, search | 1,000 per upsertList | 5-15 per account | Yes | Partial |
| SuiteTalk REST | NetSuite | HTTPS/JSON | Modern CRUD, SuiteQL queries | 1,000 per page | Shared with SOAP | Yes | No |
| RESTlets | NetSuite | HTTPS/JSON | Custom logic endpoints | Depends on script | 5 per user | Yes | No |
| SuiteQL | NetSuite | HTTPS/JSON | Analytics queries, reporting | 1,000 rows per page (100K ceiling) | Shared | Yes | No |

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | System | Notes |
|---|---|---|---|
| Max records per SOQL query page | 2,000 | Salesforce | Use queryMore/nextRecordsUrl for pagination |
| Max Composite subrequests | 25 | Salesforce | All-or-nothing by default |
| Max REST request body | 50 MB | Salesforce | |
| Max Bulk API file size | 150 MB | Salesforce | Split larger files |
| Max records per upsertList | 1,000 | NetSuite SOAP | Use batching for larger sets |
| Max records per REST page | 1,000 | NetSuite REST | Use offset pagination |
| SuiteQL max result rows | 100,000 | NetSuite | Hard ceiling; use filters to stay under |
| Apex callout timeout | 120 seconds | Salesforce | Per individual callout |

[src1, src2, src5]

### Rolling / Daily Limits

| Limit Type | Value | Window | Notes |
|---|---|---|---|
| Salesforce API calls | No daily max cap (since Spring '24) | 30-day rolling | Aggregated monthly; per-edition entitlement still applies |
| Salesforce Bulk API batches | 15,000 | 24h rolling | Shared across all editions |
| NetSuite concurrent requests | 5 (base) / 15 (SuiteCloud Plus) | Per-account, real-time | Shared across SOAP + REST + RESTlets |
| NetSuite RESTlet per-user concurrency | 5 | Per-user, real-time | Per integration user |
| NetSuite frequency throttle | ~10 requests/second sustained | 60-second window | Returns 429 when exceeded |
| Salesforce Streaming events | 100K-10M | 24h | Depends on add-on licenses |

[src1, src5]

### Transaction / Governor Limits

| Limit Type | Per-Transaction Value | System | Notes |
|---|---|---|---|
| SOQL queries | 100 | Salesforce | Includes queries from triggers -- cascading triggers consume from same pool |
| DML statements | 150 | Salesforce | Each insert/update/delete counts as 1 |
| Callouts (HTTP) | 100 | Salesforce | External HTTP requests within a transaction |
| CPU time | 10,000 ms (sync), 60,000 ms (async) | Salesforce | Exceeded = transaction abort |
| Heap size | 6 MB (sync), 12 MB (async) | Salesforce | |
| NetSuite governance units (RESTlet) | 5,000 per script execution | NetSuite | SuiteScript 2.x; Map/Reduce gets 10,000 per invocation |
| NetSuite governance units (Scheduled) | 10,000 per script execution | NetSuite | Use Map/Reduce for heavy processing |

[src1, src3, src5]

## Authentication

| System | Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|---|
| Salesforce | OAuth 2.0 JWT Bearer | Server-to-server integration (recommended) | Session timeout (default 2h) | New JWT per request | Requires Connected App + digital certificate |
| Salesforce | OAuth 2.0 Web Server | User-context operations | Access: 2h, Refresh: until revoked | Yes | Requires callback URL |
| Salesforce | Client Credentials | First-party server-to-server | Access: 2h | No | Simpler than JWT, limited scope |
| NetSuite | Token-Based Auth (TBA) | All integrations (recommended) | Does not expire | N/A -- token reused | OAuth 1.0a; consumer key/secret + token ID/secret |
| NetSuite | OAuth 2.0 | REST API integrations | Access: 60 min | Yes | Newer; not supported for SOAP |
| NetSuite | NLAuth | NEVER -- deprecated | N/A | N/A | Disallowed for new integrations since 2020 |

[src1, src2, src3]

### Authentication Gotchas

- **NetSuite TBA tokens are account-wide**: A single token set (consumer key/secret + token ID/secret) is tied to an integration record and role. If you change the role's permissions, all integrations using that token are affected. [src3]
- **Salesforce JWT flow requires per-environment certificates**: Self-signed works for sandbox; CA-signed recommended for production. The Connected App must pre-authorize the integration user. [src1]
- **NetSuite IP whitelisting can silently break integrations**: If enabled, any IP change (e.g., cloud function scaling, VPN change) produces an opaque authentication error, not a clear IP rejection. [src5]
- **OAuth 2.0 refresh tokens expire after 90 days of non-use in Salesforce**: Unattended integrations that go dormant will silently fail. Implement a keep-alive token refresh. [src1]
- **NetSuite Customer and Employee share external/internal IDs**: An ExternalID collision between these entity types causes creation failures or record overwrites. [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 primary bottleneck**: 5 concurrent requests (base) shared across ALL integrations. One chatty integration can block all others. Buy SuiteCloud Plus for 15 concurrent. [src5]
- **Salesforce Apex callout limit of 100 per transaction**: Cannot make more than 100 HTTP calls in a single transaction. Batch record changes and process asynchronously with @future or Queueable. [src1]
- **NetSuite upsertList maxes at 1,000 records**: No way to exceed this per SOAP call. For larger volumes, implement chunked batching with concurrency control. [src2]
- **Salesforce formula fields are read-only via API**: Cannot write to formula, roll-up summary, or auto-number fields. Map only to writable fields. [src1]
- **NetSuite mandatory fields vary by form**: The same record type (e.g., Sales Order) may require different fields depending on the custom form assigned. Test against the exact form your integration uses. [src3]
- **Bidirectional sync requires conflict resolution logic**: Without it, update loops (A updates B updates A) will consume API limits and cause data oscillation. Always define a "source of truth" per field. [src6]
- **NetSuite sandbox has separate concurrency and governance limits**: Lower than production. Load testing in sandbox does not accurately predict production behavior. [src5]

## Integration Pattern Decision Tree

```
START -- Salesforce <-> NetSuite O2C Integration
|
+-- What entity are you syncing?
|   +-- Accounts/Customers
|   |   +-- Direction: SF Account --> NS Customer (primary)
|   |   +-- Trigger: Account create/update in SF
|   |   +-- Method: Upsert by ExternalID (SF Account ID)
|   |   +-- ALWAYS sync customers BEFORE orders
|   |
|   +-- Products/Items
|   |   +-- Direction: NS Item --> SF Product2 (primary)
|   |   +-- NetSuite is product master
|   |   +-- Sync: Scheduled batch (items change infrequently)
|   |   +-- Match key: SKU / Item Name
|   |
|   +-- Orders (O2C core)
|   |   +-- Trigger: Opportunity Closed Won OR Order Activated in SF
|   |   +-- Direction: SF Order/Opp --> NS Sales Order
|   |   +-- Pre-check: Customer exists in NS? If no, create first
|   |   +-- Pre-check: All line items exist in NS? If no, fail with clear error
|   |   +-- Method: Create Sales Order via SOAP/REST/RESTlet
|   |   +-- Store NS Sales Order internalId back in SF custom field
|   |
|   +-- Fulfillment (sync-back)
|   |   +-- Trigger: Item Fulfillment created in NS
|   |   +-- Direction: NS --> SF (update Order status, write tracking #)
|   |   +-- Method: Scheduled poll or Platform Events
|   |
|   +-- Invoices & Payments (sync-back)
|       +-- Trigger: Invoice created / Payment applied in NS
|       +-- Direction: NS --> SF (custom objects or standard fields)
|       +-- Method: Scheduled poll
|
+-- What middleware?
|   +-- Celigo: Pre-built SF-NS app, O2C flows included
|   +-- Boomi: Certified connectors, visual mapping
|   +-- MuleSoft: Enterprise-grade, Anypoint Platform
|   +-- Workato: Recipe-based, business-user friendly
|   +-- Custom code: Apex + SuiteScript / RESTlets
|
+-- Volume?
|   +-- < 1,000 orders/day: Real-time per-record sync works
|   +-- 1,000-10,000/day: Micro-batching (every 5-15 min)
|   +-- > 10,000/day: Bulk API 2.0 (SF) + batched upsertList (NS)
|
+-- Error tolerance?
    +-- Zero-loss: ExternalID upserts + dead letter queue + idempotency keys
    +-- Best-effort: Fire-and-forget with retry (3x exponential backoff)
```

## Quick Reference: O2C Integration Flow

| Step | Source | Action | Target | Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1 | Salesforce | Account created/updated triggers sync | NetSuite | Customer (upsert by ExternalID) | Retry 3x, then DLQ |
| 2 | NetSuite | Item created/updated (scheduled) | Salesforce | Product2 (upsert by SKU) | Log mismatch, skip |
| 3 | Salesforce | Opportunity Closed-Won or Order Activated | NetSuite | Sales Order + Line Items | Validate customer/items exist first; fail fast if missing |
| 4 | NetSuite | Sales Order fulfilled (Item Fulfillment created) | Salesforce | Order status + tracking number | Poll every 5 min or event-driven |
| 5 | NetSuite | Invoice generated | Salesforce | Custom Invoice object or Order field | Scheduled batch (daily) |
| 6 | NetSuite | Payment applied (Customer Payment record) | Salesforce | Payment status on Order/Opportunity | Scheduled batch (daily) |
| 7 | Either | Credit memo / refund | Both | Refund record, order status update | Manual review recommended |

## Step-by-Step Integration Guide

### 1. Set up authentication credentials

Configure OAuth 2.0 JWT Bearer for Salesforce and Token-Based Authentication for NetSuite. [src1, src2]

```bash
# Salesforce: Create Connected App, then test JWT auth
curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${JWT_TOKEN}"

# NetSuite: Generate TBA credentials in Setup > Integration > Manage Authentication
# Consumer Key, Consumer Secret, Token ID, Token Secret
# Test with a simple GET:
curl --request GET \
  --url "https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1" \
  --header "Authorization: OAuth realm=\"${ACCOUNT_ID}\",oauth_consumer_key=\"${CONSUMER_KEY}\",oauth_token=\"${TOKEN_ID}\",oauth_nonce=\"$(uuidgen)\",oauth_timestamp=\"$(date +%s)\",oauth_signature_method=\"HMAC-SHA256\",oauth_version=\"1.0\",oauth_signature=\"${SIGNATURE}\""
```

**Verify**: Both calls return 200 OK with valid JSON response.

### 2. Sync Salesforce Accounts to NetSuite Customers

Always sync customers before orders. Use ExternalID to prevent duplicates. [src2, src3]

```javascript
// Node.js: Upsert Salesforce Account as NetSuite Customer via SuiteTalk REST
// Input:  Salesforce Account object
// Output: NetSuite Customer internalId

const upsertCustomer = async (sfAccount, nsClient) => {
  const customerPayload = {
    externalId: sfAccount.Id,                    // SF Account ID as ExternalID
    companyName: sfAccount.Name.substring(0, 83), // NS max 83 chars
    email: sfAccount.Email__c || '',
    phone: sfAccount.Phone || '',
    subsidiary: { id: '1' },                     // Required -- adjust to your subsidiary
    addressbook: {
      items: [{
        addressbookAddress: {
          addr1: sfAccount.BillingStreet || '',
          city: sfAccount.BillingCity || '',
          state: sfAccount.BillingState || '',
          zip: sfAccount.BillingPostalCode || '',
          country: mapCountryCode(sfAccount.BillingCountry) // ISO -> NS enum
        },
        defaultBilling: true,
        defaultShipping: true
      }]
    }
  };

  // Upsert: creates if not found, updates if ExternalID match exists
  const response = await nsClient.put(
    `/services/rest/record/v1/customer/eid:${sfAccount.Id}`,
    customerPayload
  );
  return response; // 200 = updated, 204 = created
};
```

**Verify**: `GET /services/rest/record/v1/customer/eid:{sfAccountId}` returns the customer record.

### 3. Sync Salesforce Order to NetSuite Sales Order

Map Salesforce Order (or Opportunity) line items to NetSuite Sales Order lines. [src3, src4]

```javascript
// Node.js: Create NetSuite Sales Order from Salesforce Order
// Input:  Salesforce Order + OrderItems
// Output: NetSuite Sales Order internalId

const createSalesOrder = async (sfOrder, sfLineItems, nsClient) => {
  // Pre-check: customer must exist
  const customer = await nsClient.get(
    `/services/rest/record/v1/customer/eid:${sfOrder.AccountId}`
  );
  if (!customer) throw new Error(`Customer not synced: ${sfOrder.AccountId}`);

  const soPayload = {
    entity: { id: customer.id },
    externalId: sfOrder.Id,
    tranDate: sfOrder.EffectiveDate,  // YYYY-MM-DD format
    otherRefNum: sfOrder.OrderNumber, // PO/reference number
    memo: `Salesforce Order ${sfOrder.OrderNumber}`,
    item: {
      items: sfLineItems.map(li => ({
        item: { externalId: li.Product2.ProductCode }, // SKU match
        quantity: li.Quantity,
        rate: li.UnitPrice,          // Decimal -- matches NS format
        amount: li.TotalPrice,
        description: li.Description || ''
      }))
    }
  };

  const response = await nsClient.post(
    '/services/rest/record/v1/salesOrder',
    soPayload
  );

  // Write NS Sales Order ID back to Salesforce
  await sfClient.patch(`/services/data/v62.0/sobjects/Order/${sfOrder.Id}`, {
    NetSuite_Sales_Order_ID__c: response.id
  });

  return response.id;
};
```

**Verify**: `GET /services/rest/record/v1/salesOrder/{id}` returns the order with correct line items.

### 4. Sync fulfillment status back to Salesforce

Poll NetSuite for new Item Fulfillment records and update Salesforce Order status. [src3]

```javascript
// Node.js: Poll NetSuite for fulfilled orders, update Salesforce
// Input:  Last sync timestamp
// Output: Updated Salesforce Orders with tracking info

const syncFulfillments = async (lastSyncTime, nsClient, sfClient) => {
  // SuiteQL: find recent fulfillments
  const query = `
    SELECT if.tranid, if.trandate, if.entity, if.createdFrom,
           so.externalId as sfOrderId,
           pkg.packageTrackingNumber
    FROM transaction if
    JOIN transaction so ON if.createdFrom = so.id
    LEFT JOIN itemFulfillmentPackage pkg ON pkg.itemFulfillment = if.id
    WHERE if.type = 'ItemShip'
      AND if.lastModifiedDate > '${lastSyncTime}'
  `;

  const fulfillments = await nsClient.suiteql(query);

  for (const f of fulfillments) {
    if (!f.sfOrderId) continue;
    await sfClient.patch(
      `/services/data/v62.0/sobjects/Order/${f.sfOrderId}`,
      {
        Status: 'Fulfilled',
        Tracking_Number__c: f.packageTrackingNumber || '',
        Fulfillment_Date__c: f.trandate
      }
    );
  }
};
```

**Verify**: Salesforce Orders show updated status and tracking numbers.

### 5. Implement error handling and retry logic

Differentiate transient errors (retry) from data errors (fail fast). [src3, src6]

```javascript
// Node.js: Retry wrapper with exponential backoff
const retryWithBackoff = async (fn, maxRetries = 5) => {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isTransient =
        error.status === 429 ||                    // Rate limited
        error.status === 503 ||                    // Service unavailable
        error.code === 'SSS_REQUEST_LIMIT_EXCEEDED' || // NS concurrency
        error.code === 'ECONNRESET' ||
        error.code === 'ETIMEDOUT';

      if (!isTransient || attempt === maxRetries - 1) {
        // Data error or max retries exhausted -- send to dead letter queue
        await deadLetterQueue.push({
          error: error.message,
          code: error.code || error.status,
          payload: fn.toString(),
          timestamp: new Date().toISOString(),
          attempt: attempt + 1
        });
        throw error;
      }

      const delay = Math.min(1000 * Math.pow(2, attempt), 60000); // Max 60s
      const jitter = delay * (0.5 + Math.random() * 0.5);
      await new Promise(resolve => setTimeout(resolve, jitter));
    }
  }
};
```

**Verify**: Transient 429 errors retry automatically; data errors go to DLQ immediately.

## Code Examples

### Python: Upsert Salesforce Account to NetSuite Customer

```python
# Input:  Salesforce Account dict (from simple_salesforce query)
# Output: NetSuite Customer internalId or error

import requests  # requests==2.31.0
from requests_oauthlib import OAuth1  # requests-oauthlib==1.3.1

def upsert_sf_account_to_ns(sf_account, ns_config):
    """Upsert Salesforce Account as NetSuite Customer using ExternalID."""
    auth = OAuth1(
        ns_config['consumer_key'],
        ns_config['consumer_secret'],
        ns_config['token_id'],
        ns_config['token_secret'],
        realm=ns_config['account_id'],
        signature_method='HMAC-SHA256'
    )

    customer = {
        'externalId': sf_account['Id'],
        'companyName': sf_account['Name'][:83],  # NS max length
        'email': sf_account.get('Email__c', ''),
        'phone': sf_account.get('Phone', ''),
        'subsidiary': {'id': '1'},
    }

    url = (
        f"https://{ns_config['account_id']}.suitetalk.api.netsuite.com"
        f"/services/rest/record/v1/customer/eid:{sf_account['Id']}"
    )

    resp = requests.put(url, json=customer, auth=auth, timeout=30)

    if resp.status_code in (200, 204):
        return {'status': 'success', 'action': 'updated' if resp.status_code == 200 else 'created'}
    elif resp.status_code == 429:
        raise RetryableError('NetSuite rate limit hit')
    else:
        raise DataError(f'NetSuite error {resp.status_code}: {resp.text}')
```

### cURL: Test NetSuite SuiteTalk REST authentication

```bash
# Input:  NetSuite account ID, TBA credentials
# Output: Customer list (limit 1) confirming auth works

# Replace placeholders with your credentials
ACCOUNT_ID="1234567"
CONSUMER_KEY="your_consumer_key"
TOKEN_ID="your_token_id"
TIMESTAMP=$(date +%s)
NONCE=$(uuidgen)

curl -s --request GET \
  --url "https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1" \
  --header "Authorization: OAuth realm=\"${ACCOUNT_ID}\",\
oauth_consumer_key=\"${CONSUMER_KEY}\",\
oauth_token=\"${TOKEN_ID}\",\
oauth_nonce=\"${NONCE}\",\
oauth_timestamp=\"${TIMESTAMP}\",\
oauth_signature_method=\"HMAC-SHA256\",\
oauth_version=\"1.0\",\
oauth_signature=\"COMPUTED_SIGNATURE\"" \
  --header "Content-Type: application/json"

# Expected: 200 OK with {"links":[...],"count":1,"items":[...]}
# If 401: check role permissions and IP whitelist
# If 429: concurrency limit hit -- wait and retry
```

### cURL: Test Salesforce OAuth 2.0 JWT Bearer auth

```bash
# Input:  Salesforce Connected App credentials + JWT assertion
# Output: Access token for API calls

curl -s -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${JWT_ASSERTION}"

# Expected: {"access_token":"...","instance_url":"https://yourorg.my.salesforce.com",...}
# Use instance_url for all subsequent API calls
```

## Data Mapping

### Field Mapping Reference

| Source Field (Salesforce) | Target Field (NetSuite) | Type | Transform | Gotcha |
|---|---|---|---|---|
| Account.Id | customer.externalId | String (18 char) | Direct | Use 18-char ID, not 15-char |
| Account.Name | customer.companyName | String | Truncate to 83 chars | NS max 83 chars vs SF 255 |
| Account.BillingStreet | addressbookAddress.addr1 | String | Direct | NS splits into addr1/addr2/addr3 (each 150 chars) |
| Account.BillingCountry | addressbookAddress.country | Enum | ISO code to NS country enum | NS uses `_unitedStates` format, not "US" |
| Account.Phone | customer.phone | String | Direct | NS allows max 100 chars |
| Order.OrderNumber | salesOrder.otherRefNum | String | Direct | Not tranId -- that is NS auto-generated |
| Order.EffectiveDate | salesOrder.tranDate | Date | YYYY-MM-DD | SF stores UTC; NS uses account timezone |
| OrderItem.Product2.ProductCode | salesOrder.item.items[].item.externalId | String | SKU match | Item must exist in NS before order sync |
| OrderItem.Quantity | salesOrder.item.items[].quantity | Number | Direct | NS rejects negative quantities on SO lines |
| OrderItem.UnitPrice | salesOrder.item.items[].rate | Currency | Direct (decimal) | Multi-currency: verify exchange rate timing |
| Opportunity.Amount | salesOrder.total | Currency | Calculated by NS from lines | Do NOT set directly -- NS calculates from line items |
| Order.Status (custom) | salesOrder.status | Enum | Lookup table | SF picklist values do NOT match NS status enums |

[src3, src4]

### Data Type Gotchas

- **DateTime timezone mismatch**: Salesforce stores all datetime fields in UTC. NetSuite uses the account's configured timezone (or individual user timezone preference). A "2026-03-02T00:00:00Z" Salesforce date could show as March 1 in a US Pacific NetSuite account. Always convert to the NS account timezone before comparing. [src3]
- **Currency precision**: Salesforce stores amounts as decimals (2 places by default). NetSuite also uses decimals but currency-dependent rounding rules may differ. Always send amounts to 2 decimal places and let NS recalculate totals from line items. [src3]
- **Country code formats**: Salesforce uses ISO 3166-1 alpha-2 codes ("US", "GB"). NetSuite uses underscore-prefixed full names ("_unitedStates", "_unitedKingdom"). Build a lookup table -- there is no automatic conversion. [src2]
- **Multi-select picklists**: Salesforce serializes as semicolon-delimited ("Value1;Value2"). NetSuite custom fields use pipe-delimited or list IDs. Parse and re-format before sending. [src3]
- **Subsidiary assignment**: NetSuite requires a subsidiary on every transaction. Salesforce has no native subsidiary concept. Map from a custom SF field or default based on business rules. [src3]

## Error Handling & Failure Points

### Common Error Codes

| Code | System | Meaning | Cause | Resolution |
|---|---|---|---|---|
| 429 | Both | Rate limit exceeded | Too many concurrent requests | Exponential backoff: wait 2^n seconds, max 5 retries |
| SSS_REQUEST_LIMIT_EXCEEDED | NetSuite | Concurrency limit breach | More than 5 (or 15) concurrent requests | Queue requests, reduce parallelism |
| INVALID_FIELD | Salesforce | Field doesn't exist or isn't writable | Wrong API version, missing field-level security | Verify field name and check FLS for integration user |
| RCRD_DSNT_EXIST | NetSuite | Record does not exist | Customer/Item not synced before order creation | Sync dependencies first (customers, then items, then orders) |
| DUPLICATE_EXTERNAL_ID | NetSuite | ExternalID already exists on different record | ID collision between Customer and Employee entities | Prefix ExternalIDs by type: "SF_ACCT_{id}", "SF_CONTACT_{id}" |
| UNABLE_TO_LOCK_ROW | Salesforce | Record locked by another transaction | Concurrent updates to same record | Retry with jitter; implement record-level locking |
| USER_ERROR | NetSuite | Missing required field | Custom form requires field not in payload | Test against exact custom form; include all mandatory fields |
| INSUFFICIENT_PERMISSION | NetSuite | Role lacks permission for operation | Integration role missing record type access | Audit role permissions; add record-level and field-level access |

[src1, src2, src3, src6]

### Failure Points in Production

- **Customer not synced before order**: The #1 failure. Salesforce Opportunity closes won, order sync fires, but the Account has never been synced to NetSuite. The Sales Order creation fails with RCRD_DSNT_EXIST for the entity field. Fix: `Always run customer sync as a prerequisite step in the same transaction or flow. Validate customer exists before creating order.` [src3]
- **Item SKU mismatch**: Product2.ProductCode in Salesforce doesn't match any NetSuite Item name/number or ExternalID. The line item creation fails. Fix: `Maintain a cross-reference table mapping SF ProductCode to NS Item internalId. Run item sync before order sync.` [src3]
- **NetSuite mandatory field varies by custom form**: A Sales Order custom form in production requires a field (e.g., Department, Class) that the sandbox form does not. Works in sandbox, fails in production. Fix: `Use the exact same custom form in both environments. Explicitly set the customForm field in API calls.` [src6]
- **Silent data truncation**: Salesforce Account.Name is 255 chars; NetSuite companyName is 83 chars. A long company name gets truncated without error, causing lookup failures later. Fix: `Explicitly truncate with logging when source length exceeds target max.` [src3]
- **Fulfillment sync-back loop**: Updating an Order in Salesforce triggers a new sync to NetSuite, which triggers a fulfillment update back, creating an infinite loop. Fix: `Use a "last_synced_by" field to skip processing records updated by the integration itself. Implement a "sync lock" flag.` [src6]
- **OAuth token expiry in long-running batches**: A batch job running for >2 hours exhausts the Salesforce access token mid-process. Fix: `Refresh the token proactively before each batch chunk (not just at start). Implement token refresh middleware.` [src1]

## Anti-Patterns

### Wrong: Creating NetSuite Sales Order without checking customer exists

```javascript
// BAD -- assumes customer is already synced
const createOrder = async (sfOrder) => {
  const soPayload = {
    entity: { externalId: sfOrder.AccountId }, // May not exist in NS!
    item: { items: [...] }
  };
  await nsClient.post('/services/rest/record/v1/salesOrder', soPayload);
};
```

### Correct: Validate customer, sync if missing, then create order

```javascript
// GOOD -- ensures dependency exists before order creation
const createOrder = async (sfOrder) => {
  // Step 1: Check if customer exists
  let customer;
  try {
    customer = await nsClient.get(
      `/services/rest/record/v1/customer/eid:${sfOrder.AccountId}`
    );
  } catch (e) {
    if (e.status === 404) {
      // Customer not synced -- sync it now
      const sfAccount = await sfClient.get(
        `/services/data/v62.0/sobjects/Account/${sfOrder.AccountId}`
      );
      customer = await upsertCustomer(sfAccount, nsClient);
    } else throw e;
  }

  // Step 2: Create order with validated customer
  const soPayload = {
    entity: { id: customer.id }, // Use internalId, not externalId
    item: { items: [...] }
  };
  await nsClient.post('/services/rest/record/v1/salesOrder', soPayload);
};
```

### Wrong: Polling all records to find changes

```javascript
// BAD -- queries ALL customers every sync cycle, wastes API calls
const syncCustomers = async () => {
  const allCustomers = await sfClient.query('SELECT Id, Name FROM Account');
  for (const acct of allCustomers.records) {
    await upsertCustomer(acct); // Processes unchanged records too
  }
};
```

### Correct: Use SystemModstamp or LastModifiedDate filter

```javascript
// GOOD -- only processes records changed since last sync
const syncCustomers = async (lastSyncTimestamp) => {
  const changed = await sfClient.query(
    `SELECT Id, Name, Phone, BillingStreet, BillingCity, BillingState,
            BillingPostalCode, BillingCountry
     FROM Account
     WHERE SystemModstamp > ${lastSyncTimestamp}
     ORDER BY SystemModstamp ASC`
  );
  for (const acct of changed.records) {
    await upsertCustomer(acct);
  }
  // Save new high-water mark for next sync
  return changed.records.at(-1)?.SystemModstamp || lastSyncTimestamp;
};
```

### Wrong: Synchronous callouts inside Salesforce triggers

```java
// BAD -- Apex trigger with synchronous callout to NetSuite
trigger OrderSync on Order (after update) {
  for (Order o : Trigger.new) {
    // This will hit governor limits and cannot be done in a trigger
    Http h = new Http();
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://netsuite.api/...');
    h.send(req); // FAILS: callouts not allowed in trigger context
  }
}
```

### Correct: Use @future or Queueable for async callouts

```java
// GOOD -- trigger queues async callout
trigger OrderSync on Order (after update) {
  Set<Id> orderIds = new Set<Id>();
  for (Order o : Trigger.new) {
    if (o.Status == 'Activated' && Trigger.oldMap.get(o.Id).Status != 'Activated') {
      orderIds.add(o.Id);
    }
  }
  if (!orderIds.isEmpty()) {
    NetSuiteOrderSync.syncOrdersAsync(orderIds); // @future or Queueable
  }
}

public class NetSuiteOrderSync {
  @future(callout=true)
  public static void syncOrdersAsync(Set<Id> orderIds) {
    // Batch callouts here -- within governor limits
  }
}
```

## Common Pitfalls

- **Sandbox != Production API limits**: NetSuite sandbox has lower concurrency and governance limits. An integration that works perfectly in sandbox may hit SSS_REQUEST_LIMIT_EXCEEDED immediately in production. Fix: `Load-test against a sandbox configured to match production service tier. Plan for at least 2x the sandbox concurrency needs.` [src5]
- **Not using ExternalID for upserts**: Creating records with "add" instead of "upsert" means any retry after a timeout creates duplicates. The first attempt may have succeeded but timed out before returning the response. Fix: `Always use upsert with ExternalID. Every record synced between systems must carry the source system's ID as its ExternalID.` [src2]
- **Ignoring NetSuite custom form assignment**: Different NetSuite custom forms have different required fields. If you don't explicitly set the form, NetSuite picks the default -- which may require fields your payload doesn't include. Fix: `Always include the customForm field in your API payload. Use the same form in sandbox and production.` [src6]
- **Multi-currency without exchange rate sync**: Salesforce and NetSuite may use different exchange rates. An order for EUR 1,000 becomes a different USD amount in each system. Fix: `Sync exchange rates from NetSuite to Salesforce (NS is the financial master). Or send amounts in the transaction currency and let NS apply its own rate.` [src3]
- **No idempotency on retry**: A network timeout at 119 seconds (Apex callout limit is 120s) means you don't know if the operation succeeded. Retrying without idempotency creates duplicates. Fix: `Use ExternalID upserts. For operations that don't support upsert, generate a unique request ID and check for its existence before retrying.` [src3]
- **Processing order line items out of sequence**: NetSuite requires items in a specific sequence when custom pricing or discount lines are involved. Fix: `Preserve the original line item sort order from Salesforce. If using discount items or subtotal lines, insert them in the correct position relative to their parent items.` [src4]

## Diagnostic Commands

```bash
# === SALESFORCE DIAGNOSTICS ===

# Check Salesforce API usage / remaining limits
curl -s -H "Authorization: Bearer ${SF_TOKEN}" \
  "${SF_INSTANCE_URL}/services/data/v62.0/limits" \
  | jq '{DailyApiRequests, ConcurrentAsyncGetReportInstances}'

# Test Salesforce authentication
curl -s -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${JWT}" | jq '.access_token'

# Verify Salesforce object/field accessibility
curl -s -H "Authorization: Bearer ${SF_TOKEN}" \
  "${SF_INSTANCE_URL}/services/data/v62.0/sobjects/Order/describe" \
  | jq '.fields[] | select(.name == "NetSuite_Sales_Order_ID__c") | {name, type, updateable}'

# === NETSUITE DIAGNOSTICS ===

# Check NetSuite API concurrency (no direct endpoint -- check via error logs)
# If you get SSS_REQUEST_LIMIT_EXCEEDED, you are at the concurrency cap
# Monitor in: Setup > Integration > Web Services Usage Log

# Test NetSuite TBA authentication
curl -s --request GET \
  "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1" \
  --header "Authorization: OAuth ..." | jq '.count'

# Verify NetSuite record exists by ExternalID
curl -s --request GET \
  "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/record/v1/customer/eid:${SF_ACCOUNT_ID}" \
  --header "Authorization: OAuth ..." | jq '{id, companyName, externalId}'

# Check NetSuite Sales Order by ExternalID
curl -s --request GET \
  "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder/eid:${SF_ORDER_ID}" \
  --header "Authorization: OAuth ..." | jq '{id, tranId, status}'

# Run SuiteQL query to find recent integration errors
# POST to /services/rest/query/v1/suiteql
curl -s -X POST \
  "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql" \
  --header "Authorization: OAuth ..." \
  --header "Content-Type: application/json" \
  --header "Prefer: transient" \
  -d '{"q": "SELECT id, tranid, type, lastModifiedDate FROM transaction WHERE type = '\''SalesOrd'\'' AND lastModifiedDate > '\''2026-03-01'\'' ORDER BY lastModifiedDate DESC"}' \
  | jq '.items[:5]'
```

## Version History & Compatibility

| Component | Version | Release Date | Status | Breaking Changes | Notes |
|---|---|---|---|---|---|
| Salesforce REST API | v62.0 | 2026-02 | Current | None | Spring '26 release |
| Salesforce REST API | v61.0 | 2025-10 | Supported | None | Winter '26 |
| Salesforce REST API | v58.0 | 2024-02 | Supported | Daily API cap removed (Spring '24) | Minimum for cap-free API usage |
| NetSuite SuiteTalk REST | 2025.1 | 2025-03 | Current | New record types added | REST coverage expanding |
| NetSuite SuiteTalk REST | 2024.2 | 2024-09 | Supported | Record-level filtering added | Check for new available filters |
| NetSuite SuiteTalk SOAP | 2025.1 | 2025-03 | Current / Stable | None | WSDL versioned per release |
| Celigo SF-NS App | 2025.3 | 2025-11 | Current | New fulfillment sync model | Check Celigo release notes |

[src1, src2, src4]

### Deprecation Policy

Salesforce supports API versions for a minimum of 3 years; retired versions return errors. NetSuite web services follow the release cycle (2 releases/year) and maintain backward compatibility within major versions. Both vendors deprecate individual API operations with at least one release cycle of notice. [src1, src2]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Salesforce is CRM + NetSuite is ERP and you need O2C automation | Both sales and finance are in NetSuite (no external CRM) | NetSuite native CRM module |
| Order volume is <50,000/day and you need near-real-time sync | Data migration of >1M historical records | Bulk API 2.0 (SF) + CSV import (NS) one-time migration |
| You have an iPaaS (Celigo, Boomi) or integration dev team | You need simple lead/contact sync only, no orders | Zapier or native connector (simpler, cheaper) |
| Multi-step O2C: quote->order->fulfillment->invoice->payment | One-way data push with no sync-back needed | Salesforce outbound message + NetSuite RESTlet |
| You need bidirectional status sync with conflict resolution | Real-time inventory sync (sub-second latency required) | Direct NetSuite inventory API or EDI-based system |

## Cross-System Comparison

| Capability | Salesforce | NetSuite | Integration Impact |
|---|---|---|---|
| API Style | REST + SOAP + Bulk + Streaming | SOAP + REST + RESTlet + SuiteQL | Must handle both REST and SOAP paradigms |
| Rate Limits | No daily cap (Spring '24+), 25 concurrent long-running | 5-15 concurrent (account-wide), frequency throttled | NetSuite is the bottleneck in most integrations |
| Bulk Import | Bulk API 2.0 (150MB/file, 15K batches/24h) | upsertList (1K records/call), CSV import via SuiteScript | SF is more mature for bulk; NS requires chunking |
| Event-Driven | Platform Events + CDC (highly mature) | User Event Scripts + SuiteScript triggers | SF has better event infrastructure; NS relies on polling |
| Auth Model | OAuth 2.0 (JWT, Web Server, Client Credentials) | TBA (OAuth 1.0a) + OAuth 2.0 (REST only) | TBA is more complex to implement than SF OAuth |
| ExternalID | Standard field on most objects | Supported on most record types | Critical for duplicate prevention on both sides |
| Sandbox | Full copy + Partial copy + Developer sandbox | Sandbox accounts (separate from production) | NS sandbox has lower limits; test carefully |
| Error Model | Structured JSON errors with error codes | SOAP faults + REST JSON errors (inconsistent) | Need separate error parsing for each system |
| API Versioning | Numbered (v62.0), 3-year minimum support | Release-based (2025.1), backward compatible | Pin both versions in integration config |
| Multi-Currency | Multi-currency org feature (dated exchange rates) | OneWorld multi-currency (subsidiary-based) | Exchange rate sync required for accurate totals |

## Important Caveats

- **NetSuite concurrency is the #1 production bottleneck**: All integrations (SOAP, REST, RESTlet) share the same concurrency pool. One poorly-designed integration can starve all others. Monitor via Setup > Integration > Web Services Usage Log. [src5]
- **Sandbox testing is unreliable for performance**: NetSuite sandbox has lower concurrency limits and may not reflect production custom forms or workflows. Always plan a production-grade load test window. [src5]
- **SuiteCloud Plus licensing affects API capacity**: Base accounts get 5 concurrent requests. SuiteCloud Plus bumps to 15. This is NOT per-integration -- it's per-account. Budget for the license if you have multiple integrations. [src5]
- **Salesforce edition affects governor limits**: Developer edition has 15,000 API calls/24h (for development only). Enterprise gets much higher allocations. Test against production-equivalent limits. [src1]
- **This card covers the standard O2C flow**: Custom commission structures, multi-tier approval workflows, and advanced revenue recognition integrations frequently break standard connectors and require custom development. [src6]
- **Rate limits and API capabilities change with each vendor release**: Always verify against current release notes before starting an implementation. This card was verified against Salesforce Spring '26 (v62.0) and NetSuite 2025.1. [src1, src2]

## Related Units

- [Salesforce REST API capabilities and rate limits](/business/erp-integration/salesforce-rest-api-capabilities/2026)
- [NetSuite SuiteTalk REST API capabilities](/business/erp-integration/netsuite-suitetalk-rest-capabilities/2026)
- [NetSuite TBA vs OAuth 2.0 authentication](/business/erp-integration/netsuite-tba-vs-oauth2/2026)
- [NetSuite SuiteScript governance limits](/business/erp-integration/netsuite-suitescript-governance/2026)
