CPQ-to-ERP Integration: Complex Order Creation from Salesforce CPQ and Oracle CPQ
Type: ERP Integration
Systems: Salesforce CPQ / Revenue Cloud, Oracle CPQ Cloud, SAP S/4HANA, NetSuite, D365
Confidence: 0.83
Sources: 7
Verified: 2026-03-07
Freshness: volatile
TL;DR
- Bottom line: CPQ-to-ERP integration converts approved quotes into ERP sales orders. Salesforce CPQ uses the
SBQQ__Quote__c.Ordered checkbox or Revenue Cloud's Order Management API; Oracle CPQ uses Commerce REST API actions (order_start, order_update) with OIC or direct REST calls to the target ERP.
- Key limit: Salesforce governor limits (100 SOQL queries per transaction) are the binding constraint — complex CPQ bundles with nested products can exhaust the limit during automated order generation. Oracle CPQ throttles above ~50 concurrent REST requests.
- Watch out for: Idempotency — duplicate order creation is the #1 production defect (30-40% of CPQ-to-ERP incidents). Always include an external reference ID (quote number + version) as a unique key on the ERP order.
- Best for: B2B organizations with complex product configurations, tiered pricing, discount approval workflows, and multi-line orders that must flow into SAP, NetSuite, D365, or Oracle ERP Cloud as structured sales orders.
- Authentication: Salesforce uses OAuth 2.0 JWT Bearer for server-to-server; Oracle CPQ uses OAuth 2.0 or basic auth with session tokens; target ERPs use their native auth.
System Profile
This integration playbook covers the CPQ-to-ERP order creation segment of the quote-to-cash cycle. It addresses two major CPQ platforms — Salesforce CPQ (including Revenue Cloud) and Oracle CPQ Cloud — and four major target ERPs: SAP S/4HANA, Oracle NetSuite, Microsoft Dynamics 365 Finance & SCM, and Oracle ERP Cloud. The focus is specifically on converting approved, fully-priced quotes into structured sales orders in the ERP, including line items, pricing, discounts, shipping, and tax classification.
| System | Role | API Surface | Direction |
| Salesforce CPQ / Revenue Cloud | CPQ — quote authoring, pricing, approvals | REST API v66.0, SBQQ ServiceRouter, Platform Events | Outbound (source) |
| Oracle CPQ Cloud | CPQ — configure, price, quote | REST API v19, Commerce API, Configuration API | Outbound (source) |
| SAP S/4HANA | ERP — sales order management | OData v4 (A_SalesOrder), BAPI, IDoc | Inbound (target) |
| Oracle NetSuite | ERP — sales order management | SuiteTalk REST/SOAP, RESTlet | Inbound (target) |
| Microsoft Dynamics 365 F&SCM | ERP — sales order management | OData v4, Dual-write, Data Entities | Inbound (target) |
| iPaaS (MuleSoft / Boomi / Celigo / OIC) | Middleware — orchestration, transformation | Pre-built connectors | Orchestrator |
API Surfaces & Capabilities
Salesforce CPQ / Revenue Cloud APIs
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
| SBQQ ServiceRouter (Apex) | Apex REST / Remoting | Quote calculation, save, read model | Governor limits | 100 SOQL/txn, 150 DML/txn | Yes | No |
| REST API v66.0 | HTTPS/JSON | CRUD on quote/order objects | 200 composite | 100K calls/24h (Enterprise) | Yes | No |
| Bulk API 2.0 | HTTPS/CSV | Mass quote line export | 150M per file | 15K batches/24h | No | Yes |
| Platform Events | Bayeux/CometD | Quote/order status notifications | N/A | Edition-dependent | Yes | N/A |
| Revenue Cloud Order Management | HTTPS/JSON | Native order creation | Per-transaction | Shared with REST API | Yes | No |
Oracle CPQ Cloud APIs
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
| Commerce REST API v19 | HTTPS/JSON | Transaction CRUD, order actions | 50 per query | Throttled (~50 concurrent) | Yes | No |
| Configuration REST API | HTTPS/JSON | Product configuration, BOM | Per-model | Shared with Commerce | Yes | No |
| Bulk Data Services (24D+) | HTTPS/CSV | Mass transaction export/import | Large batch | Separate quota | No | Yes |
| OIC Integration (pub/sub) | HTTPS/JSON + messaging | Event-driven ERP sync | Queue-based | OIC limits | Near-real-time | Yes |
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | System | Notes |
| Max composite subrequests | 25 | Salesforce Composite API | All-or-nothing by default |
| Max SOQL query results | 2,000 per page | Salesforce REST API | Use queryMore for pagination |
| Max request body size | 50 MB | Salesforce REST API | Complex bundle quotes can approach this |
| Max collection query results | 50 per page | Oracle CPQ REST API | Use offset and limit |
| Max OData batch subrequests | 1,000 | SAP S/4HANA OData | Per $batch request |
| Max SuiteTalk page size | 1,000 records | NetSuite SuiteTalk REST | Default 100 |
Rolling / Daily Limits
| Limit Type | Value | Window | System |
| API calls | 100,000 (Enterprise) / 5M (Unlimited) | 24h rolling | Salesforce |
| Bulk API batches | 15,000 | 24h rolling | Salesforce |
| Concurrent long-running requests | 25 | Per org | Salesforce |
| Concurrent REST requests | 5 (default) / 10 (SuiteCloud Plus) | Per account | NetSuite |
| OData requests | Fair-use / throttled | Per tenant | SAP S/4HANA Cloud |
| Dynamics 365 OData | 6,000 requests/5 min per user | 5-min rolling | Microsoft D365 |
| Oracle CPQ concurrent | ~50 (throttled) | Per tenant | Oracle CPQ |
Transaction / Governor Limits (Salesforce)
| Limit Type | Per-Transaction Value | Notes |
| SOQL queries | 100 | CPQ order generation triggers can cascade |
| DML statements | 150 | Each insert/update/delete = 1 |
| Callouts | 100 | HTTP requests to external services |
| CPU time | 10,000 ms (sync) / 60,000 ms (async) | Complex bundles are the main risk |
| Heap size | 6 MB (sync) / 12 MB (async) | 500+ line quotes can hit this |
| Future calls | 50 per transaction | Limits parallel async processing |
Authentication
| System | Flow | Use When | Token Lifetime | Notes |
| Salesforce | OAuth 2.0 JWT Bearer | Server-to-server middleware | 2h (session timeout) | Recommended; requires connected app + certificate |
| Oracle CPQ | OAuth 2.0 Client Credentials | Server-to-server via OIC | 1h (configurable) | Standard for OIC integrations |
| SAP S/4HANA | OAuth 2.0 + x-csrf-token | All write operations | Token: 30 min, csrf: per-session | Must fetch csrf before POST/PATCH |
| NetSuite | Token-Based Auth (TBA) | Server-to-server | Does not expire | Requires integration record + token pair |
| Dynamics 365 | OAuth 2.0 via Azure AD | All API access | Access: 1h, Refresh: 90 days | Register app in Azure AD |
Authentication Gotchas
- Salesforce JWT bearer flow requires a connected app with a digital certificate — self-signed works for dev, CA-signed recommended for prod. [src1]
- Oracle CPQ basic auth is being phased out in favor of OAuth 2.0 — plan migration before 2027. [src2]
- SAP x-csrf-token expires within the session — pooled HTTP connections each need a fresh token fetch. [src3]
- NetSuite TBA tokens do not expire but can be revoked — build re-authorization alerting into middleware. [src7]
Constraints
- Salesforce CPQ is sunset for new customers as of March 2025; no renewals beyond August 2026 — all new implementations must use Revenue Cloud.
- CPQ pricing rules and ERP pricing must match exactly — discrepancies cause 15-40% of billing errors.
- The
Ordered checkbox on SBQQ__Quote__c can only be set ONCE — subsequent attempts are ignored.
- Oracle CPQ Commerce actions execute server-side BML scripts that can modify transaction data.
- NetSuite sales order creation requires matching internal IDs for customer, item, location, and subsidiary.
- SAP
BAPI_SALESORDER_CREATEFROMDAT2 requires explicit COMMIT_WORK call — the BAPI does not auto-commit.
- Multi-currency orders require exchange rate synchronization between CPQ and ERP at order creation time.
Integration Pattern Decision Tree
START — Need to create ERP orders from CPQ quotes
├── Which CPQ platform?
│ ├── Salesforce CPQ (managed package)
│ │ ├── Order volume < 500/day?
│ │ │ ├── YES → SBQQ Ordered checkbox + trigger-based callout to ERP
│ │ │ └── NO → Platform Events + middleware (MuleSoft/Boomi) → ERP
│ │ └── Need real-time order creation?
│ │ ├── YES → Apex trigger on Order → callout to ERP REST API
│ │ └── NO → Scheduled batch: query new Orders → bulk push to ERP
│ ├── Salesforce Revenue Cloud
│ │ ├── Native to Salesforce ecosystem?
│ │ │ ├── YES → Revenue Cloud Order Management → Salesforce Order object
│ │ │ └── NO → Order Management API → middleware → ERP
│ │ └── Need external ERP sync?
│ │ ├── YES → Platform Events on Order creation → middleware → ERP API
│ │ └── NO → Stay within Salesforce ecosystem
│ └── Oracle CPQ Cloud
│ ├── Target ERP is Oracle ERP Cloud / Fusion?
│ │ ├── YES → Native OIC integration (pub/sub) → Order Management
│ │ └── NO ↓
│ ├── Using Oracle Integration Cloud (OIC)?
│ │ ├── YES → CPQ publishes to OIC queue → OIC transforms → ERP API
│ │ └── NO → CPQ Commerce REST → custom middleware → ERP API
│ └── Need real-time?
│ ├── YES → Commerce action triggers → synchronous integration
│ └── NO → Batch export via Bulk Data Services → scheduled import
├── Which target ERP?
│ ├── SAP S/4HANA → OData v4 (A_SalesOrder) or BAPI via RFC
│ ├── Oracle NetSuite → SuiteTalk REST (POST /salesOrder) or RESTlet
│ ├── Microsoft D365 → OData v4 (SalesOrderHeaders data entity)
│ └── Oracle ERP Cloud → REST API (fscmRestApi/resources/salesOrders)
└── Error strategy?
├── Zero-loss → idempotent key (quote# + version) + dead letter queue
└── Best-effort → retry 3x with exponential backoff, then alert
Quick Reference
Salesforce CPQ-to-ERP Order Creation Flow
| Step | Source System | Action | Target System | Data Objects | Failure Handling |
| 1 | Salesforce CPQ | Quote approved → Ordered checkbox → Order auto-created | Salesforce (internal) | Order, OrderItem | Governor limit check; silent failure if exceeded |
| 2 | Salesforce | Platform Event on Order creation | Middleware | Order payload (JSON) | Retry with idempotency key |
| 3 | Middleware | Transform SF Order → ERP format | Target ERP | Mapped sales order + lines | Transform errors → dead letter queue |
| 4 | Middleware | POST sales order to ERP | SAP / NetSuite / D365 | Sales order entity | 429 → backoff; validation → DLQ |
| 5 | Target ERP | Return order ID | Middleware → Salesforce | ERP Order Number | Write-back to SF Order |
Oracle CPQ-to-ERP Order Creation Flow
| Step | Source System | Action | Target System | Data Objects | Failure Handling |
| 1 | Oracle CPQ | Quote submitted → order_start action | CPQ (internal) | Transaction (Commerce doc) | BML validation; errors block submission |
| 2 | Oracle CPQ | Publishes event to OIC queue | OIC | Transaction payload | OIC error handling + retry |
| 3 | OIC/Middleware | Transform → ERP order format | Target ERP | Sales order + lines | Transform failures → error hospital |
| 4 | Middleware | POST to ERP order API | Target ERP | Sales order | Same as SF flow |
| 5 | Target ERP | Return order ID | OIC → CPQ | Order number | Write-back via order_update |
Step-by-Step Integration Guide
1. Configure Salesforce CPQ Order Generation
Set up CPQ order generation so approved quotes automatically produce Order and Order Product records. [src1, src5]
// Check quote readiness and trigger order generation
SBQQ__Quote__c quote = [
SELECT Id, SBQQ__Ordered__c, SBQQ__Primary__c, SBQQ__Status__c
FROM SBQQ__Quote__c WHERE Id = :quoteId
];
System.assert(quote.SBQQ__Primary__c == true, 'Quote must be Primary');
System.assert(quote.SBQQ__Status__c == 'Approved', 'Quote must be Approved');
quote.SBQQ__Ordered__c = true;
update quote;
// CPQ package trigger creates Order + OrderItem records automatically
Verify: SELECT Id, OrderNumber FROM Order WHERE SBQQ__Quote__c = :quoteId → expect 1 Order with Status = 'Draft'.
2. Capture Order Creation Event
Use Platform Event or Apex trigger to detect new CPQ-generated orders and push to middleware. [src1]
// Apex Trigger: Fire Platform Event on CPQ Order creation
trigger OrderCreatedTrigger on Order (after insert) {
List<CPQ_Order_Event__e> events = new List<CPQ_Order_Event__e>();
for (Order ord : Trigger.new) {
if (ord.SBQQ__Quote__c != null) {
events.add(new CPQ_Order_Event__e(
Order_Id__c = ord.Id,
Quote_Id__c = ord.SBQQ__Quote__c,
Account_Id__c = ord.AccountId
));
}
}
if (!events.isEmpty()) { EventBus.publish(events); }
}
Verify: Platform Event subscription receives event with correct Order_Id__c.
3. Transform and POST to ERP
Map Salesforce Order to target ERP schema, then create sales order. [src4, src6]
# SAP S/4HANA: Create sales order via OData v4
# Step 1: Fetch CSRF token
curl -X GET "${SAP_HOST}/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder" \
-H "Authorization: Bearer ${SAP_TOKEN}" -H "x-csrf-token: fetch" -D -
# Step 2: POST sales order with idempotency key
curl -X POST "${SAP_HOST}/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder" \
-H "Authorization: Bearer ${SAP_TOKEN}" \
-H "x-csrf-token: ${CSRF_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"SalesOrderType":"OR","SoldToParty":"CUST001",
"PurchaseOrderByCustomer":"SF-Q-00123-v1",
"to_Item":{"results":[{"Material":"MAT-001","RequestedQuantity":"5"}]}}'
Verify: HTTP 201 with SalesOrder number in response body.
4. Write ERP Order Number Back to Salesforce
Update Salesforce Order with ERP order number for cross-reference and audit trail. [src6]
// Write-back ERP order number to Salesforce
await sfClient.sobject('Order').update({
Id: sfOrderId,
ERP_Order_Number__c: erpOrderNumber,
ERP_Sync_Status__c: 'Synced',
ERP_Sync_Timestamp__c: new Date().toISOString()
});
Verify: SELECT ERP_Order_Number__c FROM Order WHERE Id = :sfOrderId returns ERP order number.
5. Handle Oracle CPQ-to-ERP via OIC
Use Commerce REST API's order actions, then route through Oracle Integration Cloud. [src2, src3]
# Oracle CPQ: Trigger order processing on approved transaction
curl -X POST \
"https://${CPQ_INSTANCE}.oracle.com/rest/v19/commerce/${PROCESS}/${MAIN_DOC}/${TXN_ID}/actions/order_start" \
-H "Authorization: Bearer ${ORACLE_CPQ_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"comments":"Order initiated from approved quote"}'
Verify: Transaction status changes; OIC dashboard shows integration flow execution.
Code Examples
Python: Salesforce CPQ Order to NetSuite Sales Order
# Input: Salesforce Order ID (from CPQ Ordered checkbox)
# Output: NetSuite Sales Order internal ID
from simple_salesforce import Salesforce # v1.12+
import requests
def sync_cpq_order_to_netsuite(sf_order_id, sf, ns_url, ns_headers):
order = sf.query(f"""
SELECT Id, OrderNumber, Account.NetSuite_Internal_ID__c,
(SELECT Product2.NetSuite_Item_ID__c, Quantity,
SBQQ__QuoteLine__r.SBQQ__NetPrice__c
FROM OrderItems)
FROM Order WHERE Id = '{sf_order_id}'
""")['records'][0]
ns_order = {
'entity': {'id': order['Account']['NetSuite_Internal_ID__c']},
'otherRefNum': order['OrderNumber'], # Idempotency key
'item': {'items': [{
'item': {'id': li['Product2']['NetSuite_Item_ID__c']},
'quantity': li['Quantity'],
'rate': str(li['SBQQ__QuoteLine__r']['SBQQ__NetPrice__c'])
} for li in order['OrderItems']['records']]}
}
resp = requests.post(ns_url + '/salesOrder', json=ns_order, headers=ns_headers)
if resp.status_code == 204:
return resp.headers.get('Location', '').split('/')[-1]
raise Exception(f'NetSuite error {resp.status_code}: {resp.text}')
JavaScript/Node.js: Salesforce CPQ Order to Dynamics 365
// Input: Salesforce Order payload (from Platform Event)
// Output: D365 Sales Order entity ID
const axios = require('axios'); // v1.6+
async function createD365SalesOrder(sfOrder, d365Config) {
const tokenResp = await axios.post(
`https://login.microsoftonline.com/${d365Config.tenantId}/oauth2/v2.0/token`,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: d365Config.clientId,
client_secret: d365Config.clientSecret,
scope: `${d365Config.resourceUrl}/.default`
})
);
const d365Order = {
SalesOrderNumber: sfOrder.OrderNumber,
OrderingCustomerAccountNumber: sfOrder.Account.D365_Customer_Number__c,
SalesOrderLines: sfOrder.OrderItems.records.map((item, idx) => ({
ItemNumber: item.Product2.D365_Item_Number__c,
SalesQuantity: item.Quantity,
SalesPrice: item.UnitPrice
}))
};
const resp = await axios.post(
`${d365Config.resourceUrl}/data/SalesOrderHeadersV2`,
d365Order,
{ headers: { 'Authorization': `Bearer ${tokenResp.data.access_token}` } }
);
return resp.data.SalesOrderNumber;
}
cURL: Test Oracle CPQ Transaction Retrieval
# Input: Oracle CPQ token, transaction ID
# Output: Transaction with line items
curl -X GET \
"https://${CPQ_INSTANCE}.oracle.com/rest/v19/commerce/${PROCESS}/${MAIN_DOC}/${TXN_ID}?expand=_lineItems" \
-H "Authorization: Bearer ${ORACLE_CPQ_TOKEN}" \
-H "Accept: application/json" | jq '{
transactionId: ._id, status: ._status,
totalPrice: .totalPrice_t, currency: .currency_t,
lineItems: [._lineItems[]? | {partNumber: .partNumber_l,
quantity: .quantity_l, netPrice: .netPrice_l}]
}'
Data Mapping
Field Mapping Reference: Salesforce CPQ to ERP
| CPQ Source Field | SAP S/4HANA | NetSuite | D365 | Type | Gotcha |
| Order.OrderNumber | PurchaseOrderByCustomer | otherRefNum | SalesOrderNumber | String | Idempotency key — must be unique in ERP |
| Account.ERP_Customer_ID__c | SoldToParty | entity.id | OrderingCustomerAccountNumber | String | NetSuite requires internalId, not externalId |
| OrderItem.Product2.ERP_Material_ID__c | Material | item.id | ItemNumber | String | SAP pads to 18 chars; NetSuite uses internalId |
| OrderItem.Quantity | RequestedQuantity | quantity | SalesQuantity | Decimal | SAP stores as string |
| OrderItem.UnitPrice | NetPriceAmount | rate | SalesPrice | Currency | Verify currency match across systems |
| SBQQ__QuoteLine__r.SBQQ__Discount__c | ConditionType ZDISC | discountRate | DiscountPercentage | % | SAP uses condition types |
| Order.CurrencyIsoCode | TransactionCurrency | currencyRecord.refName | CurrencyCode | String | NetSuite may need currency internalId |
| OrderItem.ServiceDate | RequestedDeliveryDate | expectedShipDate | RequestedShipDate | Date | SAP internal: YYYYMMDD |
| SBQQ__Quote__c.SBQQ__PaymentTerms__c | PaymentTerms | terms.refName | PaymentTermsName | String | ERP codes differ from CPQ picklist values |
Data Type Gotchas
- Salesforce amounts use major currency units (100.50 USD), but SAP may store in minor units for zero-decimal currencies (JPY). [src4]
- Oracle CPQ custom attributes use variable name suffixes (
_t for text, _l for line-level) — API field names do NOT match UI labels. [src2]
- Salesforce multi-select picklists serialize as semicolon-delimited (
Val1;Val2) via API — split on semicolons, not commas. [src1]
- SAP S/4HANA OData returns amounts as strings — parse to decimal before comparison. [src4]
Error Handling & Failure Points
Common Error Codes
| Code | System | Meaning | Resolution |
| 429 | SF, NetSuite, D365 | Rate limit exceeded | Exponential backoff; check Retry-After header |
| INVALID_FIELD | Salesforce | Field not writable | Verify SBQQ__ namespace; check FLS |
| UNABLE_TO_LOCK_ROW | Salesforce | Record locked | Retry with random jitter (500-2000ms) |
| 403 + csrf error | SAP S/4HANA | CSRF token missing/expired | Fetch new token before retry |
| DUPLICATE_VALUE | NetSuite | Duplicate external ID | Log as success — order already exists |
| SSS_REQUEST_LIMIT_EXCEEDED | NetSuite | Governance limit | Wait for next window; optimize scripts |
| INVALID_SESSION_ID | SF, Oracle CPQ | Session expired | Refresh token and retry |
Failure Points in Production
- Silent order generation failure: Setting
Ordered = true with 100+ line quotes can hit governor limits — no error thrown, checkbox stays true, but no Order created. Fix: Scheduled job to detect Ordered quotes without linked Orders. [src5]
- Duplicate ERP orders: Middleware retries after timeout, but first request succeeded. Fix:
Always include quote number + version as idempotency key; check before creating. [src4]
- Currency mismatch: CPQ in EUR, ERP customer in USD — order created with wrong amounts. Fix:
Validate currency match before order creation; include exchange rate from CPQ. [src4]
- NetSuite customer not found: First-time customer not synced yet. Fix:
Customer sync before order sync; if lookup fails, create customer first (saga pattern). [src7]
- SAP BAPI silent failure: BAPI returns RETURN table with errors but HTTP 200. Fix:
Always parse RETURN table; TYPE = 'E' or 'A' means failure. [src4]
- Oracle CPQ BML modifies data: Commerce actions can change pricing before order reaches ERP. Fix:
Post-action validation: compare CPQ total vs ERP total; alert if delta > 1%. [src2]
Anti-Patterns
Wrong: Polling Salesforce for new orders every 60 seconds
// BAD — wastes API calls, high latency, misses rapid order creation
setInterval(async () => {
const newOrders = await sf.query(
"SELECT Id FROM Order WHERE CreatedDate > LAST_N_MINUTES:1"
);
for (const order of newOrders.records) { await pushToERP(order.Id); }
}, 60000);
Correct: Use Platform Events for real-time, event-driven sync
// GOOD — real-time, no polling, no wasted API calls
const faye = require('faye');
const client = new faye.Client(sf.instanceUrl + '/cometd/66.0');
client.subscribe('/event/CPQ_Order_Event__e', async (message) => {
await pushToERP(message.data.payload.Order_Id__c);
});
Wrong: Creating ERP order without idempotency key
# BAD — retries create duplicate orders
def create_order(data):
return requests.post(erp_url + '/salesOrder', json=data).json()
Correct: Use CPQ order number as idempotency key
# GOOD — check for existing before creating
def create_order(data, sf_order_num):
existing = requests.get(erp_url + f'/salesOrder?q=ref IS "{sf_order_num}"')
if existing.json().get('count', 0) > 0:
return existing.json()['items'][0] # Already exists
data['otherRefNum'] = sf_order_num
return requests.post(erp_url + '/salesOrder', json=data).json()
Wrong: Synchronous callout inside Salesforce trigger
// BAD — blocks trigger, governor limits, mixed DML
trigger OrderTrigger on Order (after insert) {
for (Order ord : Trigger.new) {
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('https://erp.example.com/api/order');
req.setMethod('POST');
h.send(req); // Synchronous callout in trigger = BAD
}
}
Correct: Use Queueable for async callouts
// GOOD — async processing, respects governor limits
trigger OrderTrigger on Order (after insert) {
Set<Id> orderIds = new Set<Id>();
for (Order ord : Trigger.new) {
if (ord.SBQQ__Quote__c != null) orderIds.add(ord.Id);
}
if (!orderIds.isEmpty())
System.enqueueJob(new ERPOrderSyncQueueable(orderIds));
}
Common Pitfalls
- Ordered checkbox is one-shot: Cannot re-trigger by toggling off/on. Fix:
Build recovery job: query quotes where Ordered=true but no linked Order exists. [src5]
- API version mismatch: CPQ managed package API version differs from custom code. Fix:
Pin all integration code to same API version as CPQ package. [src1]
- NetSuite subsidiary mismatch: Multi-subsidiary accounts need matching subsidiary on order. Fix:
Map SF business unit to NetSuite subsidiary in customer sync. [src7]
- SAP pricing condition types: SAP uses PR00, K004, ZDISC — not simple price + discount. Fix:
Decompose CPQ net price into SAP condition type structure. [src4]
- D365 number sequence exhaustion: High-volume integration exhausts pre-allocated ranges. Fix:
Use continuous (not pre-allocated) number sequences for sales orders. [src4]
- Oracle CPQ variable name opacity: API field names use suffixes (_t, _l) that differ from UI labels. Fix:
Always reference CPQ Admin Setup for variable names. [src2]
Diagnostic Commands
# Check if CPQ order was generated from quote (Salesforce)
curl -s "${SF_INSTANCE}/services/data/v66.0/query?q=SELECT+Id,OrderNumber,Status+FROM+Order+WHERE+SBQQ__Quote__c='${QUOTE_ID}'" \
-H "Authorization: Bearer ${SF_TOKEN}" | jq '.records'
# Check Salesforce API usage / remaining limits
curl -s "${SF_INSTANCE}/services/data/v66.0/limits" \
-H "Authorization: Bearer ${SF_TOKEN}" | jq '{DailyApiRequests}'
# Check Oracle CPQ transaction status
curl -s "https://${CPQ_INSTANCE}.oracle.com/rest/v19/commerce/${PROCESS}/${MAIN_DOC}/${TXN_ID}" \
-H "Authorization: Bearer ${ORACLE_CPQ_TOKEN}" | jq '{status: ._status, total: .totalPrice_t}'
# Search SAP for order by PO number (SF order number)
curl -s "${SAP_HOST}/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?\$filter=PurchaseOrderByCustomer%20eq%20'${SF_ORDER_NUMBER}'" \
-H "Authorization: Bearer ${SAP_TOKEN}" | jq '.d.results'
# Search NetSuite for order by external reference
curl -s "https://${NS_ACCOUNT}.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder?q=otherRefNum%20IS%20%22${SF_ORDER_NUMBER}%22" \
-H "Authorization: Bearer ${NS_TOKEN}" | jq '.items'
# Check D365 sales order
curl -s "${D365_URL}/data/SalesOrderHeadersV2?\$filter=SalesOrderNumber%20eq%20'${SF_ORDER_NUMBER}'" \
-H "Authorization: Bearer ${D365_TOKEN}" | jq '.value'
Version History & Compatibility
| Component | Version | Release Date | Status | Key Changes |
| Salesforce CPQ (managed package) | v66.0 (Spring '26) | 2026-02 | Current (sunset announced) | Last major release; Revenue Cloud migration tools |
| Salesforce Revenue Cloud | Spring '26 | 2026-02 | Current (GA) | Order Management API v2; multi-currency |
| Oracle CPQ REST API | v19 (24D) | 2025-12 | Current | Bulk Data Services; enhanced Commerce actions |
| SAP S/4HANA API | 2408 | 2024-08 | Current | Async OData; enhanced A_SalesOrder |
| NetSuite SuiteTalk REST | 2024.2 | 2024-09 | Current | Enhanced SuiteQL for complex queries |
| D365 F&SCM | 10.0.39 | 2025-04 | Current | SalesOrderHeadersV2 improvements |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
| B2B with complex product configuration and approval workflows | Simple B2C eCommerce cart orders | business/erp-integration/ecommerce-to-erp/2026 |
| Multi-line quotes with bundles and negotiated pricing | Single-product fixed-price orders | Direct ERP order entry or simple webhook |
| CPQ approvals and discount governance must be preserved in ERP | No pricing complexity — ERP prices are authoritative | Skip CPQ; create orders directly in ERP |
| Need bidirectional status sync (order → fulfillment → CPQ) | One-way push without status tracking | Simple REST POST |
| Multi-currency, multi-entity orders | Single-currency, single-entity | Simplified integration without FX handling |
Cross-System Comparison
| Capability | Salesforce CPQ | Salesforce Revenue Cloud | Oracle CPQ Cloud | Notes |
| Order generation trigger | Ordered checkbox on quote | Order Management API | Commerce action (order_start) | SF CPQ is declarative; Oracle is API-driven |
| API style | REST (sObject CRUD) | REST (Order Mgmt API) | Commerce REST API v19 | Revenue Cloud API is newer |
| Pricing engine | Apex (managed package) | Salesforce native | Server-side BML | CPQ pricing must match ERP pricing |
| Native ERP integration | None (middleware required) | Salesforce-native only | Oracle ERP Cloud via OIC | Oracle CPQ + Oracle ERP = strongest native |
| Middleware options | MuleSoft, Boomi, Workato, Celigo | Same + native connectors | OIC, MuleSoft, Boomi | OIC recommended for Oracle CPQ |
| Rate limit | 100K API calls/24h (Enterprise) | Shared with REST API | Throttled (~50 concurrent) | SF has most transparent limits |
| Bulk order support | Bulk API 2.0 | Bulk API 2.0 | Bulk Data Services (24D+) | All support high volume |
| Sunset risk | Sunset Mar 2025 / Aug 2026 | Active (successor) | Active | SF CPQ customers must plan migration |
| Multi-currency | Salesforce MCE | Native | Built-in engine | All support multi-currency |
| Approval workflow | SF Approvals + CPQ Advanced | Flow-based | CPQ workflow engine | All have approval capabilities |
Important Caveats
- Salesforce CPQ is being sunset — no new customer sales since March 2025, no renewals beyond August 2026. All new implementations should use Revenue Cloud.
- CPQ-to-ERP pricing parity is non-negotiable: budget 20-30% of integration effort for pricing reconciliation.
- Sandbox/test environments have different rate limits than production across all platforms — load test with production-volume data.
- Oracle CPQ REST API field names use variable name suffixes (_t, _l, _qt) that do not match UI labels.
- Governor limits in Salesforce are per-transaction — a 200-line order with triggers can consume 80+ SOQL queries in one transaction.
- This card covers order creation only. For billing, invoicing, and revenue recognition, see quote-to-cash-integration and subscription-billing-integration.
Related Units