Sync / roster
Keep accounts, orders, and financial status consistent between NetSuite and Salesforce, with a clear system of record per object.
Sales works in Salesforce; finance works in NetSuite; both think they own the customer. Without a defined system of record per object, a two-way sync becomes a loop of overwrites. This integration establishes ownership per field, syncs accounts, orders, and status both ways, and resolves conflicts deterministically.
A working piece from this integration — no sign-up. The full build handles the edge cases, safeguards, and cutover.
NetSuite RESTlet response → Salesforce Account/Order field mapping (System of Record: NetSuite for Orders & Finance):
// NetSuite RESTlet GET /record/customer/{id} response shape { "id": "NS-CUST-88421", "companyName": "Acme Manufacturing", "externalId": "SF-001xx00000D7lHAAS", ← Salesforce Id "currency": "USD", "creditLimit": 50000, "balance": 12400.50, "stage": "CUSTOMER", → Account.Type "lastOrderDate": "2025-01-14" }
| NetSuite Field | Salesforce Object | Salesforce Field | Notes |
|---|---|---|---|
id | Account | AccountNumber | NS internal ID |
companyName | Account | Name | — |
balance | Account | AnnualRevenue | Sync direction: NS→SF |
creditLimit | Account | CreditLimit__c | Custom field |
stage | Account | Type | Map: CUSTOMER→Customer |
externalId | Account | External_Id__c | Cross-reference key |
How we'd take this from discovery to a production-safe cutover — the phases, the canonical mapping, and the edge cases that bite.
This integration maintains bidirectional consistency for Account/Customer records and Order/SalesOrder records between NetSuite and Salesforce, with NetSuite serving as the system of record for financial transactions and Salesforce for CRM activities. The sync uses NetSuite's SuiteTalk REST/Web Services APIs and Salesforce's REST and Composite APIs, coordinated through a middle-layer orchestration service.
Customer.entityId ↔ Account.AccountNumber; map Customer.companyname ↔ Account.Name. For orders, map SalesOrder.externalId ↔ Order.ExternalId__c and SalesOrder.status ↔ Order.Status. Capture all custom fields used in each org. Validate that Account.BillingAddress and Customer.billingaddress share the same latitude for geocoding if used.Account.Name and Customer.companyname, NetSuite wins on create; for Order.Status changes, NetSuite wins because it holds financial truth. Store last-modified timestamps in Customer.lastCustomSyncTime__c and Account.LastModifiedDate to detect drift. Use optimistic locking via SalesOrder.version / Order.SystemModstamp.Customer where lastModifiedDate > :lastSyncTimestamp using NetSuite SuiteQL SELECT internalId, companyname, email, entityStatus, lastModifiedDate FROM customer WHERE lastModifiedDate > '{ts}'. Push changed records to Salesforce upsert on Account.External_Id__c. Mirror the opposite direction for Opportunity closed-won triggers that generate SalesOrder in NetSuite.SalesOrder.externalId (format: SF-{Opportunity.Id}) as the idempotency key. On HTTP 409 (conflict), fetch both versions, apply priority rules, and re-upsert. Queue failures to a dead-letter table (Sync_Error_Log__c / CustomRecordSyncFailures) with error_code, payload, and retry_count. Retry with exponential backoff up to 5 attempts.Account.Type enum (Customer, Partner, Prospect) to NetSuite Customer.category via a lookup map. Translate Opportunity.StageName "Closed Won" to SalesOrder.status "Pending Fulfillment". Normalize phone formats using E.164 before write.Customer.terms in NetSuite reflects in Account.NetSuite_Payment_Terms__c in Salesforce within one sync cycle. Validate that deleting a Customer in NetSuite sets Account.Active__c = false in Salesforce (soft-delete pattern).Customer records from both systems; join on externalId; report mismatches on companyname, email, entityStatus. Fix discrepancies before go-live. Reconcile order totals: SalesOrder.total vs. Order.TotalAmount for all records created in the last 90 days.| NetSuite Field | Salesforce Field | Direction | Transform Rule |
|---|---|---|---|
Customer.internalId |
Account.NetSuite_Id__c |
NS → SF | Write on insert; read-only thereafter |
Customer.externalId |
Account.External_Id__c |
Bidirectional | Upsert key; format: SF-{Account.Id} on SF side |
Customer.companyname |
Account.Name |
NS wins on create | Coalesce if blank; prefer NS on conflict |
Customer.email |
Account.BillingEmail__c |
Bidirectional | Validate RFC 5322; skip nulls |
SalesOrder.externalId |
Order.ExternalId__c |
Bidirectional | Idempotency key; format: SF-{Opportunity.Id} |
SalesOrder.total |
Order.TotalAmount |
NS → SF | Read-only in SF; NS is financial system of record |
SalesOrder.status |
Order.Status |
NS → SF | Map: Billed → Activated; Pending Fulfillment → Draft |
Customer.entityStatus |
Account.CustomerStatus__c |
NS → SF | Reference CustomerStatus custom list in NS |
Account.Name. Salesforce allows duplicate Account.Name values; NetSuite requires unique Customer.companyname. If a sync attempt creates a duplicate in NS, the API returns SSS_UNIQUE_NAME_REQUIRED. Handle by appending Account.AccountNumber suffix or raising an exception for manual resolution.Opportunity.StageName to "Closed Won" in Salesforce while the delta-sync job runs simultaneously from NS, both systems may attempt to create SalesOrder. Use a database-level lock on Opportunity.Id and check Order.ExternalId__c existence before insert.Account.Name max is 255 chars; Customer.companyname max is 83 chars. Truncate with ellipsis and log a warning to Sync_Warning_Log__c when source exceeds 80 chars.SalesOrder.total. If SalesOrder has no line items, total may be null. Map null to Order.TotalAmount = 0 and set Order.HasErrors__c = true in Salesforce.Customer is deleted in NetSuite (hard delete), Salesforce Account retains the record. Implement a soft-delete pattern: set Account.Active__c = false in NetSuite before deletion, and sync that flag as the signal to deactivate in Salesforce.SalesOrder.trandate is stored in NetSuite's configured timezone; Order.EffectiveDate in Salesforce is UTC. Apply timezone offset using userTimeZone from the initiating user record to avoid off-by-one-day order dates.Cutover must be fully reversible and reconciled. Before the live cutover window, perform a final full extract from both systems and generate a reconciliation report keyed on externalId. Any record present in one system but not the other must be manually reviewed; do not auto-create orphans during cutover. Enable the delta-sync scheduler in "shadow mode" for 24 hours: capture changes but do not write to the target, then compare the shadow log against the live log to verify no data loss. Establish a rollback procedure: disable the sync scheduler, re-enable direct manual entry in both systems, and run a delta extract to capture any records modified during the cutover window. Set the initial lastSyncTimestamp to the cutover completion time, not a past time, to prevent replaying historical changes as new updates. Designate a single on-call engineer for each system during the 72-hour post-cutover stabilization period, with authority to halt sync and revert to manual mode if error rates exceed 1% of total volume.
Reading the reference is free. Delivering it under liability — with the safeguards that keep production running through the cutover — is what we do.