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
- 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.
- Authentication: Salesforce OAuth 2.0 JWT Bearer (server-to-server) + NetSuite Token-Based Authentication (TBA, 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.
| 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 | SuiteTalk SOAP, SuiteTalk REST, RESTlets, SuiteQL | Inbound + Outbound (fulfillment/invoice) |
| 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 | 25 subrequests | Shared with REST | Yes | No |
| Bulk API 2.0 | Salesforce | HTTPS/CSV | ETL, data migration | 150 MB per file | 15,000 batches/24h | No | Yes |
| Platform Events / CDC | Salesforce | Bayeux/CometD | Real-time notifications | 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 | 1,000 rows/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 |
| Apex callout timeout | 120 seconds | Salesforce | Per individual callout |
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 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 req/s sustained | 60-second window | Returns 429 when exceeded |
| Salesforce Streaming events | 100K-10M | 24h | Depends on add-on licenses |
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 | |
| 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]
// 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) | 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);
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
- 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 | Both sales and finance are in NetSuite | NetSuite native CRM module |
| Order volume <50K/day, need near-real-time sync | Data migration of >1M historical records | Bulk API 2.0 + CSV import (one-time) |
| You have iPaaS or integration dev team | Simple lead/contact sync only, no orders | Zapier or native connector |
| Multi-step O2C: quote->order->fulfillment->invoice->payment | One-way push with no sync-back needed | SF outbound message + NS RESTlet |
| 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.
Related Units