Salesforce-NetSuite Integration: O2C Flow, Field Mapping, Auth & Common Failures

Type: ERP Integration System: Salesforce (API v62.0) + Oracle NetSuite (2025.1) Confidence: 0.88 Sources: 7 Verified: 2026-03-02 Freshness: evolving

TL;DR

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.

SystemRoleAPI SurfaceDirection
Salesforce (API v62.0)CRM -- source of truth for customers, opportunities, quotesREST API, Composite API, Bulk API 2.0Outbound + Inbound (sync-back)
Oracle NetSuite (2025.1)ERP -- financial master for orders, fulfillment, invoicingSuiteTalk SOAP, SuiteTalk REST, RESTlets, SuiteQLInbound + Outbound (fulfillment/invoice)
Middleware (iPaaS)Integration orchestratorCeligo / Boomi / MuleSoft / WorkatoBidirectional orchestration

API Surfaces & Capabilities

API SurfaceSystemProtocolBest ForMax Records/RequestConcurrencyReal-time?Bulk?
REST API v62.0SalesforceHTTPS/JSONIndividual record CRUD, queries2,000 per SOQL page25 concurrent long-runningYesNo
Composite APISalesforceHTTPS/JSONMulti-object operations25 subrequestsShared with RESTYesNo
Bulk API 2.0SalesforceHTTPS/CSVETL, data migration150 MB per file15,000 batches/24hNoYes
Platform Events / CDCSalesforceBayeux/CometDReal-time notificationsN/AEdition-dependentYesN/A
SuiteTalk SOAPNetSuiteHTTPS/XMLFull record CRUD, upsert, search1,000 per upsertList5-15 per accountYesPartial
SuiteTalk RESTNetSuiteHTTPS/JSONModern CRUD, SuiteQL queries1,000 per pageShared with SOAPYesNo
RESTletsNetSuiteHTTPS/JSONCustom logic endpointsDepends on script5 per userYesNo
SuiteQLNetSuiteHTTPS/JSONAnalytics queries1,000 rows/page (100K ceiling)SharedYesNo

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueSystemNotes
Max records per SOQL query page2,000SalesforceUse queryMore/nextRecordsUrl for pagination
Max Composite subrequests25SalesforceAll-or-nothing by default
Max REST request body50 MBSalesforce
Max Bulk API file size150 MBSalesforceSplit larger files
Max records per upsertList1,000NetSuite SOAPUse batching for larger sets
Max records per REST page1,000NetSuite RESTUse offset pagination
SuiteQL max result rows100,000NetSuiteHard ceiling; use filters
Apex callout timeout120 secondsSalesforcePer individual callout

Rolling / Daily Limits

Limit TypeValueWindowNotes
Salesforce API callsNo daily max cap (since Spring '24)30-day rollingAggregated monthly; per-edition entitlement applies
Salesforce Bulk API batches15,00024h rollingShared across all editions
NetSuite concurrent requests5 (base) / 15 (SuiteCloud Plus)Per-account, real-timeShared across SOAP + REST + RESTlets
NetSuite RESTlet per-user concurrency5Per-user, real-timePer integration user
NetSuite frequency throttle~10 req/s sustained60-second windowReturns 429 when exceeded
Salesforce Streaming events100K-10M24hDepends on add-on licenses

Transaction / Governor Limits

Limit TypePer-Transaction ValueSystemNotes
SOQL queries100SalesforceIncludes queries from triggers -- cascading triggers consume from same pool
DML statements150SalesforceEach insert/update/delete counts as 1
Callouts (HTTP)100SalesforceExternal HTTP requests within a transaction
CPU time10,000 ms (sync) / 60,000 ms (async)SalesforceExceeded = transaction abort
Heap size6 MB (sync) / 12 MB (async)Salesforce
Governance units (RESTlet)5,000 per scriptNetSuiteSuiteScript 2.x
Governance units (Scheduled)10,000 per scriptNetSuiteUse Map/Reduce for heavy processing

Authentication

SystemFlowUse WhenToken LifetimeRefresh?Notes
SalesforceOAuth 2.0 JWT BearerServer-to-server (recommended)Session timeout (2h default)New JWT per requestRequires Connected App + certificate
SalesforceOAuth 2.0 Web ServerUser-context operationsAccess: 2h, Refresh: until revokedYesRequires callback URL
SalesforceClient CredentialsFirst-party server-to-serverAccess: 2hNoSimpler than JWT
NetSuiteToken-Based Auth (TBA)All integrations (recommended)Does not expireN/A -- reusedOAuth 1.0a; consumer + token pairs
NetSuiteOAuth 2.0REST API onlyAccess: 60 minYesNot supported for SOAP
NetSuiteNLAuthNEVER -- deprecatedN/AN/ADisallowed since 2020

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- Salesforce <-> NetSuite O2C Integration
|
+-- What entity are you syncing?
|   +-- Accounts/Customers: SF Account --> NS Customer (upsert by ExternalID)
|   +-- Products/Items: NS Item --> SF Product2 (scheduled batch, match by SKU)
|   +-- Orders (O2C core): SF Order/Opp --> NS Sales Order (pre-check customer + items)
|   +-- Fulfillment: NS Item Fulfillment --> SF Order status + tracking
|   +-- Invoices/Payments: NS --> SF (scheduled batch)
|
+-- 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: Apex + SuiteScript / RESTlets
|
+-- Volume?
|   +-- < 1,000 orders/day: Real-time per-record sync
|   +-- 1,000-10,000/day: Micro-batching (every 5-15 min)
|   +-- > 10,000/day: Bulk API 2.0 + batched upsertList
|
+-- Error tolerance?
    +-- Zero-loss: ExternalID upserts + DLQ + idempotency
    +-- Best-effort: Fire-and-forget with 3x exponential backoff

Quick Reference: O2C Integration Flow

StepSourceActionTargetData ObjectsFailure Handling
1SalesforceAccount created/updatedNetSuiteCustomer (upsert by ExternalID)Retry 3x, then DLQ
2NetSuiteItem created/updated (scheduled)SalesforceProduct2 (upsert by SKU)Log mismatch, skip
3SalesforceOpportunity Closed-Won / Order ActivatedNetSuiteSales Order + Line ItemsValidate deps first; fail fast
4NetSuiteSales Order fulfilledSalesforceOrder status + tracking #Poll every 5 min or event-driven
5NetSuiteInvoice generatedSalesforceCustom Invoice objectScheduled batch (daily)
6NetSuitePayment appliedSalesforcePayment statusScheduled batch (daily)
7EitherCredit memo / refundBothRefund + order statusManual 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]

# Salesforce: 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: Test TBA auth with simple GET
curl --request GET \
  --url "https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1" \
  --header "Authorization: OAuth realm=..."

Verify: Both calls return 200 OK with valid JSON.

2. Sync Salesforce Accounts to NetSuite Customers

Always sync customers before orders. Use ExternalID for duplicate prevention. [src2, src3]

// Upsert SF Account as NS Customer via SuiteTalk REST
const customerPayload = {
  externalId: sfAccount.Id,
  companyName: sfAccount.Name.substring(0, 83), // NS max 83 chars
  subsidiary: { id: '1' },
  addressbook: { items: [{ addressbookAddress: { /* mapped fields */ } }] }
};
await nsClient.put(`/services/rest/record/v1/customer/eid:${sfAccount.Id}`, customerPayload);

Verify: GET /customer/eid:{sfAccountId} returns the customer record.

3. Sync Salesforce Order to NetSuite Sales Order

Map Order line items to Sales Order lines. Pre-check customer and items exist. [src3, src4]

// Create NS Sales Order from SF Order
const soPayload = {
  entity: { id: customer.id },
  externalId: sfOrder.Id,
  tranDate: sfOrder.EffectiveDate,
  item: { items: sfLineItems.map(li => ({
    item: { externalId: li.Product2.ProductCode },
    quantity: li.Quantity,
    rate: li.UnitPrice
  })) }
};
const response = await nsClient.post('/services/rest/record/v1/salesOrder', soPayload);
// Write NS ID back to Salesforce
await sfClient.patch(`/sobjects/Order/${sfOrder.Id}`, { NetSuite_Sales_Order_ID__c: response.id });

Verify: GET /salesOrder/{id} returns 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]

// SuiteQL: find recent fulfillments and update SF Orders
const query = `SELECT if.tranid, 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) {
  await sfClient.patch(`/sobjects/Order/${f.sfOrderId}`, {
    Status: 'Fulfilled', Tracking_Number__c: f.packageTrackingNumber
  });
}

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]

const retryWithBackoff = async (fn, maxRetries = 5) => {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try { return await fn(); }
    catch (error) {
      const isTransient = error.status === 429 || error.status === 503
        || error.code === 'SSS_REQUEST_LIMIT_EXCEEDED';
      if (!isTransient || attempt === maxRetries - 1) {
        await deadLetterQueue.push({ error: error.message, attempt });
        throw error;
      }
      const delay = Math.min(1000 * Math.pow(2, attempt), 60000);
      await new Promise(r => setTimeout(r, delay * (0.5 + Math.random() * 0.5)));
    }
  }
};

Verify: 429 errors retry automatically; data errors go to DLQ immediately.

Code Examples

Python: Upsert Salesforce Account to NetSuite Customer

# Input:  Salesforce Account dict (from simple_salesforce)
# 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):
    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],
        'subsidiary': {'id': '1'},
    }
    url = f"https://{ns_config['account_id']}.suitetalk.api.netsuite.com/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'}
    elif resp.status_code == 429:
        raise RetryableError('Rate limit')
    else:
        raise DataError(f'{resp.status_code}: {resp.text}')

cURL: Test NetSuite TBA Authentication

# Input:  NetSuite account ID, TBA credentials
# Output: Customer list confirming auth works
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}\"..." \
  --header "Content-Type: application/json"
# Expected: 200 OK with {"count":1,"items":[...]}

cURL: Test Salesforce JWT Bearer Auth

# Input:  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"}

Data Mapping

Field Mapping Reference

Source Field (Salesforce)Target Field (NetSuite)TypeTransformGotcha
Account.Idcustomer.externalIdString (18 char)DirectUse 18-char ID, not 15-char
Account.Namecustomer.companyNameStringTruncate to 83 charsNS max 83 vs SF 255
Account.BillingStreetaddressbookAddress.addr1StringDirectNS splits into addr1/addr2/addr3
Account.BillingCountryaddressbookAddress.countryEnumISO to NS enumNS uses _unitedStates, not "US"
Order.OrderNumbersalesOrder.otherRefNumStringDirectNot tranId (NS auto-generated)
Order.EffectiveDatesalesOrder.tranDateDateYYYY-MM-DDSF UTC vs NS account timezone
OrderItem.Product2.ProductCodeitem.items[].item.externalIdStringSKU matchItem must exist in NS first
OrderItem.Quantityitem.items[].quantityNumberDirectNS rejects negative on SO lines
OrderItem.UnitPriceitem.items[].rateCurrencyDirect (decimal)Multi-currency: verify exchange rate
Opportunity.AmountsalesOrder.totalCurrencyDo NOT set -- NS calculatesNS calculates from line items
Order.Status (custom)salesOrder.statusEnumLookup tablePicklist values do NOT match

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeSystemMeaningCauseResolution
429BothRate limit exceededToo many concurrent requestsExponential backoff: 2^n seconds, max 5 retries
SSS_REQUEST_LIMIT_EXCEEDEDNetSuiteConcurrency limit breach>5 (or 15) concurrent requestsQueue requests, reduce parallelism
RCRD_DSNT_EXISTNetSuiteRecord not foundCustomer/item not synced before orderSync dependencies first
DUPLICATE_EXTERNAL_IDNetSuiteExternalID collisionID already on different record typePrefix IDs by type: SF_ACCT_{id}
INVALID_FIELDSalesforceField not writableWrong API version or missing FLSVerify field name + field-level security
UNABLE_TO_LOCK_ROWSalesforceRecord lockedConcurrent updates to same recordRetry with jitter
USER_ERRORNetSuiteMissing required fieldCustom form requires unlisted fieldTest against exact custom form
INSUFFICIENT_PERMISSIONNetSuiteRole lacks accessIntegration role missing permissionsAudit role; add record + field access

Failure Points in Production

Anti-Patterns

Wrong: Creating Sales Order without checking customer exists

// BAD -- assumes customer already synced to NetSuite
const soPayload = { entity: { externalId: sfOrder.AccountId }, item: { items: [...] } };
await nsClient.post('/services/rest/record/v1/salesOrder', soPayload); // May fail!

Correct: Validate customer, sync if missing, then create order

// GOOD -- ensures customer exists before order creation
let customer = await nsClient.get(`/customer/eid:${sfOrder.AccountId}`).catch(e => null);
if (!customer) customer = await upsertCustomer(sfAccount, nsClient); // Sync now
const soPayload = { entity: { id: customer.id }, item: { items: [...] } };
await nsClient.post('/services/rest/record/v1/salesOrder', soPayload);

Wrong: Polling all records to find changes

// BAD -- queries ALL customers every sync, wastes API calls
const allCustomers = await sfClient.query('SELECT Id, Name FROM Account');
for (const acct of allCustomers.records) await upsertCustomer(acct);

Correct: Use SystemModstamp filter for delta sync

// GOOD -- only processes changed records
const changed = await sfClient.query(
  `SELECT Id, Name, Phone FROM Account WHERE SystemModstamp > ${lastSync}`
);
for (const acct of changed.records) await upsertCustomer(acct);

Wrong: Synchronous callouts inside Salesforce triggers

// BAD -- Apex trigger with synchronous callout (FAILS in trigger context)
trigger OrderSync on Order (after update) {
  Http h = new Http();
  h.send(req); // Callouts not allowed in trigger context!
}

Correct: Use @future or Queueable for async callouts

// GOOD -- trigger queues async callout
trigger OrderSync on Order (after update) {
  Set<Id> ids = new Set<Id>();
  for (Order o : Trigger.new) if (o.Status == 'Activated') ids.add(o.Id);
  if (!ids.isEmpty()) NetSuiteOrderSync.syncOrdersAsync(ids);
}
// Separate class with @future(callout=true)

Common Pitfalls

Diagnostic Commands

# === SALESFORCE ===
# Check API usage / remaining limits
curl -s -H "Authorization: Bearer ${SF_TOKEN}" \
  "${SF_INSTANCE_URL}/services/data/v62.0/limits" | jq '{DailyApiRequests}'

# Verify 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")'

# === NETSUITE ===
# Test 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 record by ExternalID
curl -s --request GET \
  "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/record/v1/customer/eid:${SF_ID}" \
  --header "Authorization: OAuth ..." | jq '{id, companyName}'

# Check recent integration errors via SuiteQL
curl -s -X POST "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql" \
  --header "Authorization: OAuth ..." -d '{"q": "SELECT id, tranid FROM transaction WHERE type='\''SalesOrd'\'' ORDER BY lastModifiedDate DESC"}'

Version History & Compatibility

ComponentVersionReleaseStatusBreaking ChangesNotes
Salesforce REST APIv62.02026-02CurrentNoneSpring '26
Salesforce REST APIv61.02025-10SupportedNoneWinter '26
Salesforce REST APIv58.02024-02SupportedDaily cap removedMinimum for cap-free usage
NetSuite SuiteTalk REST2025.12025-03CurrentNew record typesREST coverage expanding
NetSuite SuiteTalk SOAP2025.12025-03Current / StableNoneWSDL versioned
Celigo SF-NS App2025.32025-11CurrentNew fulfillment modelCheck release notes

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Salesforce is CRM + NetSuite is ERP, need O2C automationBoth sales and finance are in NetSuiteNetSuite native CRM module
Order volume <50K/day, need near-real-time syncData migration of >1M historical recordsBulk API 2.0 + CSV import (one-time)
You have iPaaS or integration dev teamSimple lead/contact sync only, no ordersZapier or native connector
Multi-step O2C: quote->order->fulfillment->invoice->paymentOne-way push with no sync-back neededSF outbound message + NS RESTlet
Need bidirectional status sync with conflict resolutionReal-time inventory sync (sub-second latency)Direct NS inventory API or EDI

Cross-System Comparison

CapabilitySalesforceNetSuiteIntegration Impact
API StyleREST + SOAP + Bulk + StreamingSOAP + REST + RESTlet + SuiteQLHandle both paradigms
Rate LimitsNo daily cap (Spring '24+), 25 concurrent5-15 concurrent (account-wide)NS is the bottleneck
Bulk ImportBulk API 2.0 (150MB, 15K batches/24h)upsertList (1K records/call)SF more mature; NS needs chunking
Event-DrivenPlatform Events + CDC (mature)User Event Scripts + triggersSF better events; NS relies on polling
Auth ModelOAuth 2.0 (JWT, Web Server, Client Creds)TBA (OAuth 1.0a) + OAuth 2.0 (REST only)TBA more complex to implement
ExternalIDStandard on most objectsSupported on most recordsCritical for duplicate prevention
SandboxFull + Partial + DeveloperSeparate accounts (lower limits)NS sandbox unreliable for perf testing
Error ModelStructured JSON with codesSOAP faults + REST JSON (inconsistent)Need separate error parsing
API VersioningNumbered (v62.0), 3-year supportRelease-based (2025.1), backward compatPin both in config
Multi-CurrencyMulti-currency org (dated rates)OneWorld (subsidiary-based)Exchange rate sync required

Important Caveats

Related Units