Order-to-Cash (O2C) Cross-System Integration Playbook

Type: ERP Integration System: Salesforce + SAP S/4HANA + NetSuite + Dynamics 365 Confidence: 0.88 Sources: 7 Verified: 2026-03-02 Freshness: evolving

TL;DR

System Profile

This integration playbook covers the four most common O2C system combinations in enterprise environments. The Order-to-Cash cycle spans order management, credit management, fulfillment, invoicing, payment collection, and reconciliation — touching 4–6 systems minimum. The CRM is the entry point (order capture), the ERP is the system of record for financials, the WMS handles physical fulfillment, and the billing/AR system manages invoicing and collections.

SystemRoleAPI SurfaceDirection
Salesforce Sales CloudCRM — order capture, customer 360REST API v62.0, Platform EventsOutbound (orders) / Inbound (status)
SAP S/4HANA CloudERP — financial master, fulfillment orchestrationOData v4 (API_SALES_ORDER_SRV)Inbound (orders) / Outbound (status)
Oracle NetSuiteERP — mid-market alternative to SAPSuiteTalk SOAP, RESTlets, REST APIInbound (orders) / Outbound (status)
Dynamics 365 F&SCMERP — Microsoft ecosystemOData v4, Data Entities, Business EventsInbound (orders) / Outbound (status)
iPaaS (MuleSoft/Boomi/Workato/Celigo)Middleware — orchestration, transformation, error handlingREST/SOAP connectorsOrchestrator
WMS / 3PLWarehouse — pick, pack, shipREST API (system-dependent)Inbound (fulfillment) / Outbound (shipment)

API Surfaces & Capabilities

SystemAPI SurfaceProtocolBest ForRate LimitReal-time?Bulk?
Salesforce REST APIRESTHTTPS/JSONIndividual order CRUD, composite operations100K calls/24h (Enterprise)YesNo
Salesforce Bulk API 2.0BulkHTTPS/CSVHistorical order migration, >2K records15K batches/24hNoYes
Salesforce Platform EventsPub/SubBayeux/gRPCOrder status change notifications100K-10M events/24hYesN/A
SAP S/4HANA OData v4ODataHTTPS/JSONSales order CRUD, deep insertThrottled per tenant configYesVia batch
SAP S/4HANA IDocEDI/XMLRFC/HTTPLegacy batch order exchangeNo hard limit (queued)NoYes
NetSuite SuiteTalk SOAPSOAPHTTPS/XMLFull record CRUD, bulk lists (25/call)5 concurrent (default)YesLimited
NetSuite RESTletsRESTHTTPS/JSONCustom multi-step server-side logicShared with SOAP concurrencyYesCustom
Dynamics 365 OData v4ODataHTTPS/JSONSales order via Data Entities6,000 req/5min per userYesVia $batch
Dynamics 365 Business EventsPub/SubAzure Service Bus/WebhooksStatus change notificationsNo hard limitYesN/A

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueSystemNotes
Max records per SOQL query2,000SalesforceUse queryMore/nextRecordsUrl for pagination
Max composite subrequests25SalesforceAll-or-nothing or independent mode
Max records per SOAP addList25NetSuiteSuiteTalk bulk operations
Max OData $batch requests1,000SAP S/4HANAChangesets within a batch
Max request body size50 MBSalesforce RESTSplit larger payloads
Max SuiteQL result rows5,000NetSuitePaginate with offset/hasMore
Max Dynamics 365 $batch1,000 operationsDynamics 365Individual changesets within batch

Rolling / Daily Limits

Limit TypeValueWindowSystem
API calls100,000 (Enterprise), 5M (Unlimited)24h rollingSalesforce
Bulk API batches15,00024h rollingSalesforce
Platform Events published100K-10M (depends on license)24hSalesforce
Concurrent requests5 (default), 10+ (SuiteCloud Plus)Per accountNetSuite
API governance units5,000 units/script (scheduled)Per executionNetSuite
Service protection limit6,000 requests/5minPer userDynamics 365

Transaction / Governor Limits (Salesforce)

Limit TypePer-Transaction ValueNotes
SOQL queries100 (sync), 200 (async)Includes queries from triggers — cascading triggers consume same pool
DML statements150Each insert/update/delete counts as 1, regardless of record count
Callouts (HTTP)100External API calls within a single transaction
CPU time10,000 ms (sync), 60,000 ms (async)Exceeded = transaction rollback
Heap size6 MB (sync), 12 MB (async)Watch large order payloads
Future calls50 per transaction@future methods for async callouts

Authentication

SystemFlowUse WhenToken LifetimeRefresh?
SalesforceOAuth 2.0 JWT BearerServer-to-server iPaaS integrationSession timeout (default 2h)New JWT per request
SalesforceOAuth 2.0 Web ServerUser-context operationsAccess: 2h, Refresh: until revokedYes
NetSuiteToken-Based Auth (OAuth 1.0a)All production integrationsDoes not expireN/A — permanent
NetSuiteOAuth 2.0Modern REST API integrationsAccess: 60 minYes
SAP S/4HANAOAuth 2.0 + x-csrf-tokenAll OData write operationsConfigurable per tenantYes
Dynamics 365Azure AD OAuth 2.0 (client credentials)Server-to-serverDefault 60-90 minYes

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START — Implementing Order-to-Cash across CRM + ERP + WMS + Billing
├─ What CRM → ERP trigger?
│   ├ Opportunity Closed-Won
│   │   ├ Real-time needed? → Platform Events / Apex trigger + iPaaS webhook
│   │   └ Batch OK? → Scheduled job every 15-60 min
│   ├ Order record approved (CPQ flow)
│   │   └ Trigger on Order.Status = 'Activated'
│   └ Manual push (button click)
│       └ Apex callout via iPaaS → synchronous order creation
├ What's the daily order volume?
│   ├ < 500 orders/day → Real-time event-driven is fine
│   ├ 500-5,000 orders/day → Event-driven + batch fallback for peaks
│   └ > 5,000 orders/day → Batch/bulk processing required
├ Which ERP?
│   ├ SAP S/4HANA → OData v4 API_SALES_ORDER_SRV deep insert
│   ├ NetSuite → SuiteTalk SOAP / RESTlet
│   ├ Dynamics 365 → OData v4 SalesOrderHeaders + Lines
│   └ Other ERP → REST/SOAP or file-based (CSV/EDI) for legacy
├ Need fulfillment status back to CRM?
│   ├ Real-time → ERP webhooks/events → iPaaS → SF REST update
│   └ Batch → Scheduled sync every 15-60 min
└ Error tolerance?
    ├ Zero-loss → Idempotency keys + dead letter queue + manual review
    └ Best-effort → Retry 3x with exponential backoff, alert on failure

Quick Reference

StepSource SystemActionTarget SystemData ObjectsFailure Handling
1. Order captureSalesforceOpportunity Closed-Won or Order ActivatediPaaSOpportunity/Order + Line ItemsN/A — CRM event
2. Customer synciPaaSUpsert customer (create if new)ERPAccount → CustomerRetry 3x, then DLQ
3. Order creationiPaaSCreate sales order with line itemsERPOrder → Sales OrderIdempotency key, retry 3x, DLQ
4. Order confirmationERPReturn order number + statusSalesforceSales Order ID → Order fieldUpdate CRM with ERP order #
5. Fulfillment triggerERPRelease order to warehouseWMS / 3PLPick list / fulfillment orderManual review if inventory short
6. Ship confirmationWMSShip complete → tracking numberERP + SalesforceShipment + trackingAlert if no confirmation in 24h
7. Invoice creationERPAuto-generate invoice on shipBilling / ARInvoiceAlert if auto-invoice fails
8. Payment receiptPayment gatewayPayment applied to invoiceERP + SalesforcePayment → Cash applicationReconciliation queue for mismatches
9. Revenue recognitionERPRevenue schedule triggeredGL / FinanceJournal entriesFinance team review

Step-by-Step Integration Guide

1. Set up authentication across all systems

Configure OAuth credentials for each system. Use dedicated integration users with minimal permissions. Store all credentials in the iPaaS credential vault. [src1, src6]

# Test Salesforce auth (JWT bearer flow)
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}"

# SAP: Fetch x-csrf-token before writes
curl -X GET "https://{host}/sap/opu/odata4/sap/api_sales_order_srv/srvd_a2x/sap/salesorder/0001/" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "x-csrf-token: Fetch" -c cookies.txt

Verify: Each system returns a valid access token. Salesforce returns {"access_token": "...", "instance_url": "..."}.

2. Implement customer master sync (CRM → ERP)

Before creating orders, ensure the customer exists in the ERP. Use upsert keyed on cross-system external ID to prevent duplicates. [src1]

def sync_customer_to_netsuite(sf_account):
    existing = netsuite.search("customer",
        filters=[("externalId", "is", sf_account["Id"])])
    if existing:
        netsuite.update("customer", existing[0]["internalId"], {
            "companyName": sf_account["Name"][:83],  # NS max 83 chars
            "subsidiary": map_subsidiary(sf_account["Legal_Entity__c"]),
        })
        return existing[0]["internalId"]
    else:
        return netsuite.create("customer", {
            "externalId": sf_account["Id"],
            "companyName": sf_account["Name"][:83],
        })["internalId"]

Verify: Customer exists in ERP with correct external ID.

3. Create sales order in ERP with idempotency

Transform Salesforce order data into ERP sales order format. Include idempotency key (Salesforce Order ID) as external ID. [src1, src4]

# SAP S/4HANA: OData v4 deep insert (header + line items)
payload = {
    "SalesOrderType": "OR",
    "SalesOrganization": map_sales_org(sf_order["Legal_Entity__c"]),
    "SoldToParty": sf_order["ERP_Customer_ID__c"],
    "PurchaseOrderByCustomer": sf_order["PO_Number__c"],
    "_Item": [
        {"Material": item["ERP_Material_ID__c"],
         "RequestedQuantity": str(item["Quantity"])}
        for item in sf_line_items
    ]
}
response = requests.post(url, json=payload, headers=headers)

Verify: SAP returns 201 with SalesOrder number.

4. Wire the event trigger (CRM → iPaaS)

Configure CRM to fire an event when an order is ready. Salesforce Platform Events or CDC are preferred over polling. [src6]

// Apex trigger fires when Order status changes to 'Activated'
trigger OrderActivatedTrigger on Order (after update) {
    List<Order_Integration_Event__e> events = new List<>();
    for (Order o : Trigger.new) {
        Order old = Trigger.oldMap.get(o.Id);
        if (o.Status == 'Activated' && old.Status != 'Activated') {
            events.add(new Order_Integration_Event__e(
                Order_Id__c = o.Id,
                Idempotency_Key__c = o.Id + '_' + o.LastModifiedDate
            ));
        }
    }
    if (!events.isEmpty()) EventBus.publish(events);
}

Verify: Platform Event published. Check subscriptions in Salesforce Setup.

5. Implement fulfillment status sync (ERP → CRM)

Sync fulfillment status from ERP back to Salesforce so sales reps see real-time order progress. [src1, src3]

# Poll ERP for fulfilled orders, update Salesforce (runs every 15 min)
def sync_fulfillment_to_salesforce(last_run):
    fulfilled = netsuite.search("salesOrder",
        filters=[("status", "is", "Billed"),
                 ("lastModifiedDate", "onOrAfter", last_run)])
    sf_updates = [{"Id": o["externalId"],
                   "ERP_Status__c": map_status(o["status"]),
                   "Tracking_Numbers__c": o["trackingNumbers"]}
                  for o in fulfilled]
    for chunk in chunks(sf_updates, 25):
        salesforce.composite_update("Order", chunk)

Verify: Salesforce Order shows updated ERP_Status__c and Tracking_Numbers__c.

6. Implement error handling with dead letter queue

All steps must handle transient errors (retry) vs. permanent errors (dead letter queue). [src1]

def execute_with_retry(operation, payload, idempotency_key):
    for attempt in range(3):
        try:
            return operation(payload)
        except RetryableError:
            time.sleep(2 ** (attempt + 1))  # 2s, 4s, 8s
        except PermanentError as e:
            send_to_dlq(idempotency_key, payload, str(e))
            return None
    send_to_dlq(idempotency_key, payload, "Max retries exhausted")

Verify: Failed messages in DLQ with payload + error. DLQ count < 1% of total volume.

Code Examples

Python: Salesforce-to-NetSuite order sync

# Input:  Salesforce Order ID (from Platform Event)
# Output: NetSuite Sales Order internal ID

def sync_order_sf_to_netsuite(sf_order_id, sf_conn, ns_config):
    order = sf_conn.query(
        f"SELECT Id, AccountId, TotalAmount, CurrencyIsoCode, "
        f"PO_Number__c, ERP_Customer_ID__c "
        f"FROM Order WHERE Id = '{sf_order_id}'"
    )["records"][0]
    items = sf_conn.query(
        f"SELECT Product2.ERP_Item_ID__c, Quantity, UnitPrice "
        f"FROM OrderItem WHERE OrderId = '{sf_order_id}'"
    )["records"]

    result = netsuite_upsert("salesOrder", {
        "externalId": sf_order_id,  # Idempotency key
        "entity": {"internalId": order["ERP_Customer_ID__c"]},
        "item": {"item": [
            {"item": {"internalId": i["Product2"]["ERP_Item_ID__c"]},
             "quantity": i["Quantity"], "rate": str(i["UnitPrice"])}
            for i in items
        ]}
    }, ns_config)

    sf_conn.Order.update(sf_order_id, {
        "ERP_Order_Number__c": result["tranId"],
        "ERP_Status__c": "Pending Fulfillment"
    })
    return result["internalId"]

JavaScript/Node.js: Dynamics 365 order creation

// Input:  Order data from CRM (transformed)
// Output: Dynamics 365 Sales Order ID

const axios = require('axios');
const { ClientSecretCredential } = require('@azure/identity');

async function createD365SalesOrder(orderData, config) {
  const credential = new ClientSecretCredential(
    config.tenantId, config.clientId, config.clientSecret);
  const token = await credential.getToken(`${config.d365Url}/.default`);

  const header = await axios.post(
    `${config.d365Url}/data/SalesOrderHeadersV2`,
    { SalesOrderNumber: orderData.idempotencyKey,
      OrderingCustomerAccountNumber: orderData.customerAccount,
      CurrencyCode: orderData.currency },
    { headers: { Authorization: `Bearer ${token.token}` } });

  for (const line of orderData.lineItems) {
    await axios.post(
      `${config.d365Url}/data/SalesOrderLines`,
      { SalesOrderNumber: header.data.SalesOrderNumber,
        ItemNumber: line.erpItemId,
        OrderedSalesQuantity: line.quantity },
      { headers: { Authorization: `Bearer ${token.token}` } });
  }
  return header.data.SalesOrderNumber;
}

cURL: Test SAP S/4HANA Sales Order creation

# Step 1: Fetch CSRF token
curl -s -X GET \
  "https://{sap_host}/sap/opu/odata4/sap/api_sales_order_srv/srvd_a2x/sap/salesorder/0001/" \
  -H "Authorization: Bearer ${SAP_TOKEN}" \
  -H "x-csrf-token: Fetch" -c cookies.txt -D headers.txt
CSRF=$(grep -i 'x-csrf-token' headers.txt | awk '{print $2}' | tr -d '\r')

# Step 2: Create order with deep insert
curl -s -X POST \
  "https://{sap_host}/.../SalesOrder" \
  -H "Authorization: Bearer ${SAP_TOKEN}" \
  -H "x-csrf-token: ${CSRF}" \
  -H "Content-Type: application/json" -b cookies.txt \
  -d '{"SalesOrderType":"OR","SoldToParty":"10100001",
       "_Item":[{"Material":"TG11","RequestedQuantity":"10"}]}'
# Expected: 201 Created

Data Mapping

Field Mapping Reference

Source (Salesforce)Target (NetSuite)Target (SAP)Target (D365)Gotcha
Account.Namecustomer.companyNameSoldToParty (lookup)OrderingCustomerAccountNumberNetSuite max 83 chars; SAP uses customer number not name
OrderItem.UnitPricesalesOrderItem.rateNetAmountSalesPriceDecimal precision: SF=2, NS=2, SAP=2-3, D365=2-6
OrderItem.QuantitysalesOrderItem.quantityRequestedQuantityOrderedSalesQuantityUoM must match: SF "Each" → SAP "EA" → NS "Each"
Product2.ProductCodeitem.itemId (via externalId)Material (number)ItemNumberAlways use cross-reference table
Order.CurrencyIsoCodecurrency (internal ID)TransactionCurrencyCurrencyCodeNetSuite uses internal IDs, not ISO codes
Payment_Terms__cterms (internal ID)CustomerPaymentTermsPaymentTermsMap labels ("Net 30") to ERP codes
Order.ShipToAddressshipAddressShipToParty (lookup)DeliveryAddressSAP uses Ship-To partner function
Order.TotalAmountDO NOT MAPDO NOT MAPDO NOT MAPLet ERP recalculate from line items

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeMeaningSystemResolution
429Rate limit exceededSalesforceExponential backoff: wait 2^n seconds, max 5 retries
DUPLICATE_VALUEExternal ID already existsSF / NetSuiteExpected on retry — upsert handles this
INSUFFICIENT_ACCESSMissing permissionsAllAudit integration user profile/role
SSS_REQUEST_LIMIT_EXCEEDEDGovernance units breachedNetSuiteReduce script complexity or split operations
CX_BAPI_ERRORBusiness rule violationSAPCheck customer credit status; may need manual override
403 ForbiddenMissing x-csrf-token or permsSAP / D365Re-fetch csrf (SAP); check app registration (D365)
UNABLE_TO_LOCK_ROWRecord locked by another txnSalesforceRetry with random jitter (0-2s)

Failure Points in Production

Anti-Patterns

Wrong: Polling Salesforce for new orders every minute

# BAD — burns 1,440 API calls/day just for polling, adds latency
while True:
    new_orders = salesforce.query("SELECT Id FROM Order WHERE Status = 'Activated'")
    for order in new_orders: process_order(order)
    time.sleep(60)

Correct: Event-driven trigger via Platform Events

# GOOD — zero API calls for detection; fires instantly on status change
# iPaaS subscribes to Platform Events via CometD/gRPC Pub/Sub API
# API calls used only for actual data retrieval and order creation

Wrong: Mapping Salesforce totals to ERP order totals

# BAD — CRM total may not match ERP calculation (tax, discounts, rounding)
erp_order = {"total": sf_order["TotalAmount"], "tax": sf_order["Tax__c"]}

Correct: Push line items only; let ERP calculate totals

# GOOD — ERP applies its own pricing, tax, and discount rules
erp_order = {"items": [{"material": i["sku"], "quantity": i["qty"],
    "unit_price": i["price"]} for i in sf_line_items]}

Wrong: Individual API calls for each order item

# BAD — 10 line items = 10 API calls, burns rate limit
for item in sf_line_items:
    netsuite.create("salesOrderItem", transform(item))

Correct: Deep insert / composite request (all items in one call)

# GOOD — 1 API call creates header + all line items atomically
netsuite.create("salesOrder", {
    "externalId": sf_order_id,
    "item": {"item": [transform(i) for i in sf_line_items]}})

Common Pitfalls

Diagnostic Commands

# Salesforce: Check API usage / remaining limits
curl -s "https://{instance}.salesforce.com/services/data/v62.0/limits" \
  -H "Authorization: Bearer ${SF_TOKEN}" | jq '.DailyApiRequests'

# SAP S/4HANA: Test connectivity and auth
curl -s -o /dev/null -w "%{http_code}" \
  "https://{sap_host}/sap/opu/odata4/sap/api_sales_order_srv/..." \
  -H "Authorization: Bearer ${SAP_TOKEN}"
# Expected: 200

# SAP: Verify sales order exists
curl -s "https://{sap_host}/.../SalesOrder('{ORDER_NUMBER}')" \
  -H "Authorization: Bearer ${SAP_TOKEN}" | jq '.SalesOrder'

# Dynamics 365: Check service health
curl -s "https://{d365_host}/data/SalesOrderHeadersV2?\$top=1" \
  -H "Authorization: Bearer ${D365_TOKEN}" -o /dev/null -w "%{http_code}"

# iPaaS DLQ depth (platform-specific):
# MuleSoft: Anypoint Monitoring > DLQ depth
# Boomi: Process Reporting > Errors tab
# Workato: Jobs > Failed > filter by recipe

Version History & Compatibility

System / APIVersionReleaseStatusBreaking Changes
Salesforce REST API v62.0Spring '262026-02CurrentNone
Salesforce REST API v61.0Winter '262025-10SupportedNone
SAP API_SALES_ORDER_SRV (OData v4)24082024-08CurrentAsync processing added
SAP API_SALES_ORDER_SRV (OData v2)A2X2020+SupportedUse v4 for new integrations
NetSuite 2024.22024.22024-07CurrentREST API GA for more record types
D365 F&SCM 10.0.3910.0.392024-03CurrentNew SalesOrderHeadersV2 entity

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Multiple systems handle different O2C stepsSingle ERP handles entire O2C nativelyBuilt-in ERP O2C workflows
Order volume > 50/day justifies automation< 10 orders/day, simple productsManual order entry or CSV import
Need real-time order status across sales + opsBatch nightly sync is acceptableScheduled file-based integration
Multi-entity / multi-currency operationsSingle entity, single currencySimplified point-to-point integration
Complex pricing / fulfillment logic in ERPSimple flat-rate pricing in CRMCRM-native invoicing

Cross-System Comparison

CapabilitySF + SAPSF + NetSuiteSF + D365Notes
Pre-built connectorSAP CPI / MuleSoftCeligo / Boomi / WorkatoMicrosoft Dual-writeCeligo most mature for SF+NS
Order creation APIOData v4 deep insertSuiteTalk SOAP / RESTletOData v4 Data EntitiesSAP and D365 share OData pattern
Real-time eventsSAP Business EventsUser Event ScriptsBusiness Events via Service BusSAP newest; NS most custom
Bulk order support$batch (1,000 ops)addList (25 records/call)$batch (1,000 ops)NetSuite most limited
IdempotencyPurchaseOrderByCustomerexternalId (native upsert)SalesOrderNumberNetSuite most elegant
Concurrency limitTenant-configurable5 default / 10+ Plus6,000 req/5min per userNetSuite most restrictive
Auth complexityOAuth + x-csrf (2-step)TBA (OAuth 1.0a)Azure AD OAuth 2.0SAP most complex
iPaaS ecosystemMuleSoft, SAP CPICeligo, Boomi, WorkatoPower Automate, Logic AppsMost choice for SF+NS
Avg implementation12-20 weeks8-14 weeks10-16 weeksNetSuite fastest (mid-market)
Typical cost$150K-$500K$50K-$200K$100K-$350KHighly variable by complexity

Important Caveats

Related Units