Salesforce-NetSuite Integration: O2C Flow, Field Mapping, Auth & Common Failures
How do you integrate Salesforce and NetSuite - 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.
Watch out for: Salesforce Account is NOT equal to a NetSuite Customer -- field-level mapping mismatches (name length 255 vs 83, address format, picklist values) cause silent data corruption.
Best for: Order-to-cash automation where Salesforce owns the sales pipeline and NetSuite owns fulfillment, invoicing, and financial close.
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.
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 + Inbound (sync-back)
Oracle NetSuite (2025.1)
ERP -- financial master for orders, fulfillment, invoicing
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
Governance units (RESTlet)
5,000 per script
NetSuite
SuiteScript 2.x
Governance units (Scheduled)
10,000 per script
NetSuite
Use Map/Reduce for heavy processing
Authentication
System
Flow
Use When
Token Lifetime
Refresh?
Notes
Salesforce
OAuth 2.0 JWT Bearer
Server-to-server (recommended)
Session timeout (2h default)
New JWT per request
Requires Connected App + 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
NetSuite
Token-Based Auth (TBA)
All integrations (recommended)
Does not expire
N/A -- reused
OAuth 1.0a; consumer + token pairs
NetSuite
OAuth 2.0
REST API only
Access: 60 min
Yes
Not supported for SOAP
NetSuite
NLAuth
NEVER -- deprecated
N/A
N/A
Disallowed since 2020
Authentication Gotchas
NetSuite TBA tokens are account-wide: changing the role's permissions affects all integrations using that token. [src3]
Salesforce JWT flow requires per-environment certificates: self-signed for sandbox, CA-signed for production. [src1]
NetSuite IP whitelisting can silently break integrations -- any IP change produces opaque authentication errors. [src5]
OAuth 2.0 refresh tokens expire after 90 days of non-use in Salesforce -- implement a keep-alive token refresh. [src1]
NetSuite Customer and Employee share external/internal IDs -- collisions cause creation failures. [src2]
Constraints
NetSuite concurrency is the primary bottleneck: 5 concurrent requests (base) shared across ALL integrations. One chatty integration can block all others.
Salesforce Apex callout limit: 100 HTTP calls per transaction. Batch changes and process asynchronously.
NetSuite upsertList maxes at 1,000 records: For larger volumes, implement chunked batching.
Salesforce formula fields are read-only via API: Cannot write to formula, roll-up summary, or auto-number fields.
NetSuite mandatory fields vary by custom form: Test against the exact form your integration uses.
Bidirectional sync requires conflict resolution: Without it, update loops consume API limits and cause data oscillation.
NetSuite sandbox has separate (lower) limits: Load testing in sandbox does not predict production behavior.
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
Step
Source
Action
Target
Data Objects
Failure Handling
1
Salesforce
Account created/updated
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 / Order Activated
NetSuite
Sales Order + Line Items
Validate deps first; fail fast
4
NetSuite
Sales Order fulfilled
Salesforce
Order status + tracking #
Poll every 5 min or event-driven
5
NetSuite
Invoice generated
Salesforce
Custom Invoice object
Scheduled batch (daily)
6
NetSuite
Payment applied
Salesforce
Payment status
Scheduled batch (daily)
7
Either
Credit memo / refund
Both
Refund + order status
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]
# 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]
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]
# 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)
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 vs SF 255
Account.BillingStreet
addressbookAddress.addr1
String
Direct
NS splits into addr1/addr2/addr3
Account.BillingCountry
addressbookAddress.country
Enum
ISO to NS enum
NS uses _unitedStates, not "US"
Order.OrderNumber
salesOrder.otherRefNum
String
Direct
Not tranId (NS auto-generated)
Order.EffectiveDate
salesOrder.tranDate
Date
YYYY-MM-DD
SF UTC vs NS account timezone
OrderItem.Product2.ProductCode
item.items[].item.externalId
String
SKU match
Item must exist in NS first
OrderItem.Quantity
item.items[].quantity
Number
Direct
NS rejects negative on SO lines
OrderItem.UnitPrice
item.items[].rate
Currency
Direct (decimal)
Multi-currency: verify exchange rate
Opportunity.Amount
salesOrder.total
Currency
Do NOT set -- NS calculates
NS calculates from line items
Order.Status (custom)
salesOrder.status
Enum
Lookup table
Picklist values do NOT match
Data Type Gotchas
DateTime timezone mismatch: Salesforce stores UTC; NetSuite uses account timezone. A midnight UTC date can show as previous day in US Pacific accounts. [src3]
Country code formats: Salesforce uses ISO alpha-2 ("US"); NetSuite uses underscore-prefixed names ("_unitedStates"). Build a lookup table. [src2]
Multi-select picklists: Salesforce semicolon-delimited; NetSuite pipe-delimited or list IDs. Parse and re-format before sending. [src3]
Subsidiary assignment: NetSuite requires subsidiary on every transaction; Salesforce has no native concept. Map from custom field or default by 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: 2^n seconds, max 5 retries
SSS_REQUEST_LIMIT_EXCEEDED
NetSuite
Concurrency limit breach
>5 (or 15) concurrent requests
Queue requests, reduce parallelism
RCRD_DSNT_EXIST
NetSuite
Record not found
Customer/item not synced before order
Sync dependencies first
DUPLICATE_EXTERNAL_ID
NetSuite
ExternalID collision
ID already on different record type
Prefix IDs by type: SF_ACCT_{id}
INVALID_FIELD
Salesforce
Field not writable
Wrong API version or missing FLS
Verify field name + field-level security
UNABLE_TO_LOCK_ROW
Salesforce
Record locked
Concurrent updates to same record
Retry with jitter
USER_ERROR
NetSuite
Missing required field
Custom form requires unlisted field
Test against exact custom form
INSUFFICIENT_PERMISSION
NetSuite
Role lacks access
Integration role missing permissions
Audit role; add record + field access
Failure Points in Production
Customer not synced before order: The #1 failure. Order sync fires but Account not in NetSuite. Fix: Always run customer sync as prerequisite. Validate customer exists before order creation. [src3]
Item SKU mismatch: ProductCode in Salesforce doesn't match any NetSuite Item. Fix: Maintain cross-reference table. Run item sync before order sync. [src3]
NetSuite mandatory field varies by custom form: Works in sandbox, fails in production. Fix: Use exact same custom form. Explicitly set customForm field. [src6]
Silent data truncation: SF Account.Name (255 chars) truncated to NS companyName (83 chars). Fix: Explicitly truncate with logging when source exceeds target max. [src3]
Fulfillment sync-back loop: Updating SF Order triggers new NS sync, creating infinite loop. Fix: Use "last_synced_by" field. Implement sync lock flag. [src6]
OAuth token expiry in long-running batches: Batch exceeding 2h exhausts SF token. Fix: Refresh token proactively before each chunk. [src1]
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);
// 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
Sandbox != Production API limits: NetSuite sandbox has lower concurrency. Fix: Load-test against production-equivalent service tier. [src5]
Not using ExternalID for upserts: "add" instead of "upsert" creates duplicates on retry after timeout. Fix: Always use upsert with ExternalID for all cross-system records. [src2]
Ignoring custom form assignment: Different forms require different fields. Fix: Always include customForm in API payload. Use same form in all environments. [src6]
Multi-currency without exchange rate sync: Different rates in each system. Fix: Sync exchange rates from NetSuite (financial master). Send amounts in transaction currency. [src3]
No idempotency on retry: Network timeout at 119s (limit is 120s) means unknown success. Fix: Use ExternalID upserts. Generate unique request IDs for non-upsert operations. [src3]
Line items out of sequence: NS requires specific ordering for discount/subtotal lines. Fix: Preserve original sort order. Insert discount lines after their parent items. [src4]
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
Component
Version
Release
Status
Breaking Changes
Notes
Salesforce REST API
v62.0
2026-02
Current
None
Spring '26
Salesforce REST API
v61.0
2025-10
Supported
None
Winter '26
Salesforce REST API
v58.0
2024-02
Supported
Daily cap removed
Minimum for cap-free usage
NetSuite SuiteTalk REST
2025.1
2025-03
Current
New record types
REST coverage expanding
NetSuite SuiteTalk SOAP
2025.1
2025-03
Current / Stable
None
WSDL versioned
Celigo SF-NS App
2025.3
2025-11
Current
New fulfillment model
Check release notes
When to Use / When Not to Use
Use When
Don't Use When
Use Instead
Salesforce is CRM + NetSuite is ERP, need O2C automation
Need bidirectional status sync with conflict resolution
Real-time inventory sync (sub-second latency)
Direct NS inventory API or EDI
Cross-System Comparison
Capability
Salesforce
NetSuite
Integration Impact
API Style
REST + SOAP + Bulk + Streaming
SOAP + REST + RESTlet + SuiteQL
Handle both paradigms
Rate Limits
No daily cap (Spring '24+), 25 concurrent
5-15 concurrent (account-wide)
NS is the bottleneck
Bulk Import
Bulk API 2.0 (150MB, 15K batches/24h)
upsertList (1K records/call)
SF more mature; NS needs chunking
Event-Driven
Platform Events + CDC (mature)
User Event Scripts + triggers
SF better events; NS relies on polling
Auth Model
OAuth 2.0 (JWT, Web Server, Client Creds)
TBA (OAuth 1.0a) + OAuth 2.0 (REST only)
TBA more complex to implement
ExternalID
Standard on most objects
Supported on most records
Critical for duplicate prevention
Sandbox
Full + Partial + Developer
Separate accounts (lower limits)
NS sandbox unreliable for perf testing
Error Model
Structured JSON with codes
SOAP faults + REST JSON (inconsistent)
Need separate error parsing
API Versioning
Numbered (v62.0), 3-year support
Release-based (2025.1), backward compat
Pin both in config
Multi-Currency
Multi-currency org (dated rates)
OneWorld (subsidiary-based)
Exchange rate sync required
Important Caveats
NetSuite concurrency is the #1 production bottleneck -- all integrations share the same pool. Monitor via Setup > Integration > Web Services Usage Log.
Sandbox testing is unreliable for performance -- NetSuite sandbox has lower limits and may not reflect production custom forms or workflows.
SuiteCloud Plus licensing affects API capacity -- base accounts get 5 concurrent, SuiteCloud Plus gets 15. This is per-account, not per-integration.
Salesforce edition affects governor limits -- Developer edition has 15,000 API calls/24h (dev only). Enterprise gets much higher allocations.
This card covers the standard O2C flow -- custom commission structures, multi-tier approvals, and advanced revenue recognition require custom development.
Rate limits and API capabilities change with each vendor release -- always verify against current release notes. Verified against SF Spring '26 (v62.0) and NS 2025.1.