Quote-to-Cash is a cross-system integration playbook, not a single-system API reference. It covers the end-to-end data flow from the moment a sales rep configures a quote through final revenue recognition in the general ledger. The canonical QTC flow involves a CPQ system, a CRM, an ERP, a billing platform, and a revenue recognition engine. This card covers the most common architecture patterns across Salesforce Revenue Cloud/CPQ, Oracle NetSuite, SAP S/4HANA, and Zuora.
| System | Role | API Surface | Direction |
|---|---|---|---|
| Salesforce Revenue Cloud / CPQ | CPQ — configure, price, quote | REST API v62.0, Platform Events | Outbound (quotes, orders) |
| Salesforce CRM (Sales Cloud) | CRM — customer master, opportunity | REST API v62.0 | Bidirectional |
| Oracle NetSuite | ERP — order management, fulfillment, GL | SuiteTalk REST, RESTlet | Inbound (orders), Outbound (fulfillment status) |
| SAP S/4HANA | ERP — financials, supply chain | OData v4, Business Events | Inbound (orders), Outbound (fulfillment, GL) |
| Zuora | Billing — subscriptions, invoicing, payments | REST API v1, Revenue API | Inbound (subscriptions), Outbound (invoices) |
| Middleware (MuleSoft / Boomi / Workato) | Orchestrator — routing, transformation, error handling | N/A | Orchestrator |
Each system in the QTC chain exposes different API surfaces. The integration architect must select the right API per leg of the journey.
| Integration Leg | Source System | Target System | Recommended API | Latency | Volume |
|---|---|---|---|---|---|
| Quote → Order | Salesforce CPQ | Salesforce Order Mgmt | Internal (Apex/Flow) | <1s | Real-time |
| Order → ERP | Salesforce | NetSuite | SuiteTalk REST + Middleware | 2-10s | Event-driven |
| Order → ERP | Salesforce | SAP S/4HANA | OData v4 + Middleware | 2-15s | Event-driven or batch |
| Order → Billing | Salesforce | Zuora | Zuora REST API v1 | 2-5s | Event-driven |
| Fulfillment → CRM | NetSuite/SAP | Salesforce | REST API v62.0 | 2-10s | Event-driven |
| Invoice → GL | Zuora / Billing | NetSuite/SAP | Journal Entry API | Batch (hourly/daily) | Batch |
| Revenue Schedule → GL | Zuora Revenue | NetSuite/SAP | Revenue API + GL API | Batch (daily) | Batch |
| Payment → AR | Payment Gateway | ERP | Webhook + REST | Near-real-time | Event-driven |
| System | Limit Type | Value | Notes |
|---|---|---|---|
| Salesforce | Daily API calls | 100,000 (Enterprise), 5,000,000 (Unlimited) | 24h rolling window. QTC flows consume 10-50 calls per order. |
| Salesforce | Governor limits (SOQL per txn) | 100 queries | Complex CPQ bundles with triggers can exhaust this in a single save. |
| Salesforce | Platform Events publish | 250,000/day (Enterprise) | Each order activation can publish 1-5 events. |
| NetSuite | SuiteTalk REST concurrency | 10 (standard), 25 (SuiteCloud Plus) | Throttled per account, not per user. |
| NetSuite | RESTlet execution | 5,000 governance units per script | Long-running transforms exhaust this quickly. |
| SAP S/4HANA | OData batch requests | 1,000 operations per $batch | Split larger order sets across batches. |
| Zuora | REST API rate limit | 40 concurrent requests per tenant | Shared across all integrations. |
| Zuora | Revenue API | 500 records per bulk request | Batch revenue schedules in groups of 500. |
| Limit Type | Salesforce | NetSuite | SAP S/4HANA | Zuora |
|---|---|---|---|---|
| Daily API calls | 100K-5M (edition) | No hard daily limit (concurrency-throttled) | No hard daily limit (fair use) | No hard daily limit (concurrency-throttled) |
| Bulk import | Bulk API 2.0: 150MB/file | CSV Import: 25K records/file | FBDI: 250MB/file | 500 records/batch |
| Webhooks / Events | Platform Events: 250K-10M/day | User Event Scripts: per-record | Business Events: per-config | Callout Notifications: 1,000/hour |
| System | Recommended Flow | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Salesforce | OAuth 2.0 JWT Bearer | Session timeout (2h default) | New JWT per request | Server-to-server; use Connected App with certificate. |
| NetSuite | Token-Based Authentication (TBA) | No expiry (until revoked) | N/A | Consumer key + token pair. |
| SAP S/4HANA Cloud | OAuth 2.0 Client Credentials | Configurable (typically 12h) | Yes | Communication Arrangement required. |
| Zuora | OAuth 2.0 Client Credentials | 1 hour | Yes | Client ID + Client Secret; endpoint: /oauth/token. |
START — Implement Quote-to-Cash integration
├── Which CPQ system?
│ ├── Salesforce CPQ / Revenue Cloud
│ │ ├── ERP is NetSuite → Salesforce-to-NetSuite playbook (most common)
│ │ ├── ERP is SAP S/4HANA → Salesforce-to-SAP playbook (enterprise)
│ │ └── ERP is Dynamics 365 → Salesforce-to-D365 playbook
│ ├── SAP CPQ → SAP-native QTC (CPQ → S/4HANA → Billing)
│ └── Other CPQ (DealHub, Conga) → API-first middleware pattern
├── What billing model?
│ ├── One-time / perpetual → CPQ → Order → ERP Invoice → GL
│ ├── Subscription (fixed) → CPQ → Billing Platform → ERP GL
│ ├── Usage-based → CPQ → Billing + Metering → Rating → Invoice → GL
│ └── Hybrid → CPQ → Split: recurring to billing, one-time to ERP → GL
├── What integration pattern?
│ ├── Event-driven (recommended) → Platform Events → Middleware → ERP
│ ├── Real-time API → Direct REST calls, max 200 records/operation
│ ├── Batch → Bulk API / CSV Import / FBDI, nightly or hourly
│ └── Hybrid (event for orders, batch for reconciliation) → RECOMMENDED
├── Revenue recognition required?
│ ├── YES (ASC 606 / IFRS 15) → Dedicated rev rec integration leg
│ └── NO → Invoice → GL is sufficient
└── Error tolerance?
├── Zero-loss (financial data) → Idempotent + DLQ + reconciliation
└── Best-effort → Fire-and-forget with retry
| Step | Source System | Action | Target System | Key Data Objects | Failure Handling |
|---|---|---|---|---|---|
| 1. Configure & Price | CPQ | Rep configures products, pricing engine calculates | CPQ | Quote, Quote Lines | Validation errors to rep |
| 2. Approve & Sign | CPQ + CLM | Quote approved, contract generated, e-signed | CPQ / DocuSign | Quote, Contract | Rejection loops back to rep |
| 3. Create Order | CRM / CPQ | Closed-won triggers order creation | Order Management | Order, Order Products | Retry via Platform Event |
| 4. Sync to ERP | Middleware | Order data transformed and sent to ERP | ERP (NetSuite/SAP) | Sales Order, SO Lines | Retry 3x → DLQ → alert |
| 5. Fulfill | ERP | Inventory allocated, picked, shipped | ERP + Shipping | Item Fulfillment, Shipment | Backorder handling |
| 6. Create Subscription | Middleware | If recurring: create subscription | Billing (Zuora) | Subscription, Rate Plan | Idempotency check |
| 7. Generate Invoice | Billing / ERP | Invoice generated per billing schedule | Billing or ERP | Invoice, Invoice Items | Bill run retry |
| 8. Collect Payment | Billing / Payment | Payment gateway processes charge | Payment Gateway / AR | Payment, Application | Dunning sequence |
| 9. Recognize Revenue | Rev Rec Engine | Obligations fulfilled → revenue recognized | GL (ERP) | Revenue Schedule, JE | Hold if incomplete |
| 10. Reconcile | All Systems | Automated reconciliation job | Data Warehouse | Reconciliation Report | Variance alerts |
Map data objects across all systems. The quote line item in CPQ must carry enough data to create a sales order in ERP, a subscription in billing, and a revenue schedule in rev rec — all without manual re-entry. [src3]
CPQ Quote Line → maps to:
├── ERP Sales Order Line (product, qty, price, delivery date)
├── Billing Subscription Charge (billing frequency, start/end, proration)
└── Rev Rec Obligation (performance obligation type, recognition pattern)
Required fields on every quote line:
- Product SKU (unified across all systems)
- Unit price (net of discounts)
- Quantity, Start date / End date
- Billing frequency, Tax classification code
- Revenue recognition template / pattern
- Billing entity (for multi-entity)
Verify: Every quote line produces a complete record in ERP, billing, AND rev rec without any manual field entry.
Deploy MuleSoft, Boomi, Workato, or Celigo as the central orchestrator. Never build point-to-point connections between CPQ, ERP, and billing. [src6]
Architecture:
Salesforce CPQ ──Platform Event──→ Middleware
├──→ NetSuite (Sales Order)
├──→ Zuora (Subscription)
└──→ Rev Rec Engine (Obligation)
Middleware responsibilities:
1. Event consumption 4. Error handling (retry, DLQ, alerting)
2. Data transformation 5. Idempotency enforcement
3. Orchestration 6. Logging (every message in/out/error)
Verify: Middleware can receive a test Platform Event from Salesforce and log it successfully.
When a Salesforce opportunity closes and an order is created, the middleware picks up the Platform Event and creates a sales order in the ERP. [src1]
// Middleware: Salesforce Order → NetSuite Sales Order
async function handleOrderEvent(event) {
const sfOrderId = event.payload.Order_Id__c;
// Idempotency check
const existing = await netsuite.get('/salesOrder', {
q: `externalId IS ${sfOrderId}`
});
if (existing.count > 0) return existing.items[0].id;
// Fetch, transform, create with retry
const order = await salesforce.get(`/services/data/v62.0/sobjects/Order/${sfOrderId}`);
const nsOrder = transformToNetSuite(order);
return await retryWithBackoff(() => netsuite.post('/salesOrder', nsOrder),
{ maxRetries: 3, baseDelay: 2000 });
}
Verify: Create test order in Salesforce sandbox → sales order appears in NetSuite within 30 seconds.
For subscription and usage-based products, the middleware creates a subscription in the billing platform from the same order event. [src4]
// Middleware: Salesforce Order → Zuora Subscription
async function createSubscription(order, recurringItems) {
const subscription = {
accountKey: order.zuoraAccountId,
contractEffectiveDate: order.startDate,
subscribeToRatePlans: recurringItems.map(item => ({
productRatePlanId: mapToZuoraRatePlan(item.Product2Id),
chargeOverrides: [{
price: item.UnitPrice, quantity: item.Quantity
}]
})),
externallyManagedBy: order.sfOrderId // idempotency key
};
return await zuora.post('/v1/subscriptions', subscription);
}
Verify: Create test order with recurring product → subscription appears in Zuora with correct rate plan.
Map performance obligations to the revenue recognition engine per ASC 606's 5-step model. [src5]
ASC 606 Performance Obligation Mapping:
Quote Line Type → Rev Rec Pattern → Recognition Trigger
Perpetual license → Point-in-time → Delivery/activation
Subscription (SaaS) → Over time (straight-line) → Ratably over term
Professional services → Over time (% complete) → Milestone completion
Usage-based → As invoiced (variable) → Monthly meter read
Verify: Mixed-obligation contract → correct recognition schedules generated for each line type.
Automated reconciliation catches errors before audit findings. Run daily or hourly depending on volume. [src3]
# Reconciliation: Compare orders across CPQ, ERP, Billing
def reconcile_qtc(start_date, end_date):
sf_orders = salesforce.query(f"SELECT Id,OrderNumber,TotalAmount FROM Order ...")
ns_orders = netsuite.search('salesOrder', {'tranDate': ...})
variances = []
for sf in sf_orders:
ns = find_by_external_id(ns_orders, sf['Id'])
if not ns:
variances.append({'type': 'MISSING_IN_ERP', 'severity': 'critical'})
elif abs(sf['TotalAmount'] - ns['total']) > 0.01:
variances.append({'type': 'AMOUNT_MISMATCH', 'severity': 'high'})
return variances
Verify: Run on last 7 days → zero MISSING_IN_ERP and zero AMOUNT_MISMATCH variances.
# Input: Salesforce Platform Event payload (Order activated)
# Output: ERP Sales Order + Billing Subscription + Rev Rec Schedule
class QTCOrchestrator:
def process_order(self, order_id):
order = self.sf.get_order(order_id)
items = self.sf.get_order_items(order_id)
one_time = [i for i in items if i['Billing_Frequency__c'] == 'One-Time']
recurring = [i for i in items if i['Billing_Frequency__c'] != 'One-Time']
erp_order_id = self._create_erp_order(order, items) # Idempotent
sub_id = self._create_subscription(order, recurring) if recurring else None
self._map_rev_rec(order, items, erp_order_id, sub_id)
self.sf.update_order(order_id, {
'ERP_Order_Id__c': erp_order_id,
'Billing_Subscription_Id__c': sub_id,
'Integration_Status__c': 'Synced'
})
# Test Salesforce — fetch recent activated orders
curl -s -H "Authorization: Bearer $SF_TOKEN" \
"https://yourorg.my.salesforce.com/services/data/v62.0/query?q=SELECT+Id,OrderNumber,TotalAmount+FROM+Order+WHERE+Status='Activated'+LIMIT+5"
# Test NetSuite — search by external ID
curl -s -H "Authorization: Bearer $NS_TOKEN" \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder?q=externalId+IS+sf-order-001"
# Test Zuora — fetch subscriptions for account
curl -s -H "Authorization: Bearer $ZUORA_TOKEN" \
"https://rest.zuora.com/v1/subscriptions/accounts/ACCOUNT_KEY"
| CPQ Field (Salesforce) | ERP Field (NetSuite) | Billing Field (Zuora) | Type | Gotcha |
|---|---|---|---|---|
| Quote.Name | salesOrder.tranId | — | String | NetSuite tranId max 45 chars |
| QuoteLine.Product2Id | salesOrderItem.item | ratePlanCharge.productRatePlanChargeId | Reference | SKU must exist in target system first |
| QuoteLine.UnitPrice | salesOrderItem.rate | chargeOverride.price | Currency | Exchange rate source must match |
| QuoteLine.Quantity | salesOrderItem.quantity | chargeOverride.quantity | Number | Decimal: SF 2dp, NS 4dp |
| QuoteLine.StartDate | salesOrder.startDate | subscription.serviceActivationDate | Date | TZ: SF=UTC, NS=user-pref, Zuora=tenant |
| QuoteLine.EndDate | salesOrder.endDate | subscription.termEndDate | Date | Null = evergreen — handle explicitly |
| QuoteLine.Billing_Frequency__c | — (derived) | ratePlanCharge.billingPeriod | Enum | "Quarterly" in CPQ may not exist in Zuora |
| QuoteLine.Tax_Code__c | salesOrderItem.taxCode | — (tax engine) | Reference | Use Avalara/Vertex for consistency |
| QuoteLine.Discount__c | salesOrderItem.rate (net) | chargeOverride.discountAmount | %/Amount | CPQ % discount vs ERP absolute mismatch |
| Account.BillingAddress | customer.defaultBillingAddress | account.billToContact | Address | NS requires internal ID for state/country |
null. NetSuite returns "". Zuora omits the field. Middleware must treat all three as equivalent.| System | Code | Meaning | Resolution |
|---|---|---|---|
| Salesforce | UNABLE_TO_LOCK_ROW | Record locked by another transaction | Retry with jitter (100-500ms). Check for trigger recursion. |
| Salesforce | LIMIT_EXCEEDED | Governor limit hit | Bulkify — process records in batches. |
| NetSuite | SSS_REQUEST_LIMIT_EXCEEDED | Concurrency limit hit | Queue requests; implement throttling in middleware. |
| NetSuite | INVALID_KEY_OR_REF | Foreign key not found | Sync master data (customers, products) BEFORE orders. |
| Zuora | 50000040 | Duplicate subscription | Idempotency working — log and skip. |
| Zuora | 50000060 | Invalid rate plan | Sync product catalog to Zuora before subscription creation. |
| Any | 429 | Rate limit exceeded | Exponential backoff; check retry-after header. |
| Middleware | TIMEOUT | Target system did not respond | Backoff: 2s, 4s, 8s, 16s, max 5 retries → DLQ. |
Dead letter queue + hourly reconciliation. [src3]Add validation rule on Quote: IF(IsClone__c, require re-approval). [src3]Build explicit bundle-to-line mapping in middleware. [src6]Map discount reason codes to billing discount objects. Include expiry dates. [src3]Fulfillment-to-billing feedback loop before next invoice run. [src3]Carry selling_entity_id from quote through every system. [src7]// BAD — creates O(n^2) connections, unmaintainable
CPQ ──→ ERP CPQ ──→ Billing CPQ ──→ Rev Rec
ERP ──→ Billing ERP ──→ CRM Billing ──→ Rev Rec
// 6 systems = 30 potential connections
// GOOD — single orchestration layer, O(n) connections
CPQ ──→ Middleware ──→ ERP
──→ Billing
──→ Rev Rec
// 6 systems = 6 connections through middleware
// BAD — one slow system blocks entire chain (9-25s total)
async function onOrderActivated(orderId) {
const nsOrder = await createNetSuiteOrder(orderId); // 5-15s
const zuoraSub = await createZuoraSubscription(orderId); // 2-5s
const revRec = await mapRevenueRecognition(orderId); // 2-5s
}
// GOOD — fire event, return immediately; process in parallel
async function onOrderActivated(orderId) {
await salesforce.publishEvent('Order_Activated__e', { Order_Id__c: orderId });
// Middleware handler (separate process):
const [erpResult, billingResult] = await Promise.all([
createNetSuiteOrder(orderId),
createZuoraSubscription(orderId)
]);
await mapRevenueRecognition(orderId, erpResult, billingResult);
}
// BAD — names change, differ between systems, have typos
const nsItem = await netsuite.find('item', {
name: sfOrderItem.Product2.Name // "Acme Widget Pro" vs "ACME Widget Pro v2"
});
// GOOD — external ID is immutable, system-agnostic
const nsItem = await netsuite.find('item', {
externalId: sfOrderItem.Product2.ProductCode // "WIDGET-PRO-001"
});
Include Finance and Billing teams in CPQ design from day 1. [src3]Test all lifecycle scenarios: new, amend, renew, cancel, suspend, resume. [src3]Build product catalog sync as the FIRST integration. [src6]Parse every bulk response for per-record success/failure. Failed records go to DLQ. [src6]Document exchange rate policy and enforce consistently. [src7]Design the amendment flow as a first-class integration pattern.# Check Salesforce orders with integration errors
curl -s -H "Authorization: Bearer $SF_TOKEN" \
"https://yourorg.my.salesforce.com/services/data/v62.0/query?q=SELECT+Id,OrderNumber,Integration_Status__c+FROM+Order+WHERE+Integration_Status__c='Error'+LIMIT+10"
# Check Salesforce API usage
curl -s -H "Authorization: Bearer $SF_TOKEN" \
"https://yourorg.my.salesforce.com/services/data/v62.0/limits" | jq '.DailyApiRequests'
# Check NetSuite for missing orders
curl -s -H "Authorization: Bearer $NS_TOKEN" \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder?q=externalId+IS+sf-order-id"
# Check Zuora subscription status
curl -s -H "Authorization: Bearer $ZUORA_TOKEN" \
"https://rest.zuora.com/v1/subscriptions/SUB_KEY" | jq '{status, contractEffectiveDate}'
# Count orders by integration status (last 7 days)
curl -s -H "Authorization: Bearer $SF_TOKEN" \
"https://yourorg.my.salesforce.com/services/data/v62.0/query?q=SELECT+Integration_Status__c,COUNT(Id)+FROM+Order+WHERE+CreatedDate=LAST_N_DAYS:7+GROUP+BY+Integration_Status__c"
| Component | Current Version | Release Date | Breaking Changes | Notes |
|---|---|---|---|---|
| Salesforce Revenue Cloud | Spring 2026 | 2026-02 | Replaces CPQ Billing with native billing | GA; CPQ package still supported, no new features |
| Salesforce CPQ (managed package) | 240+ | 2025-10 | None (maintenance mode) | Migrate to Revenue Cloud for new implementations |
| Salesforce REST API | v62.0 | 2026-02 | None | Min v58.0 for Revenue Cloud objects |
| NetSuite SuiteTalk REST | 2024.2 | 2024-08 | Changed auth header format | REST is GA; SOAP still supported |
| SAP S/4HANA Cloud | 2408 | 2024-08 | Key User Extensibility changes | OData v4 preferred |
| Zuora REST API | v2024-09 | 2024-09 | Orders API GA replaces subscribe() | Use Orders API for new integrations |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Complex B2B deals with multi-line quotes and approvals | Simple B2C e-commerce checkout | Direct ERP-to-payment-gateway integration |
| Subscription or usage-based billing models | One-time product sales only | Standard ERP order-to-cash |
| ASC 606 / IFRS 15 with multiple performance obligations | Cash-basis accounting | ERP native invoicing + GL journals |
| Multi-system landscape (separate CPQ, ERP, billing) | Single-vendor suite (all NetSuite or all SAP) | Native suite integration |
| >100 orders/day with SLA requirements | <10 orders/day, manual acceptable | Manual order entry + spreadsheet reconciliation |
| Multi-currency, multi-entity, multi-geo operations | Single currency, single entity | Simplified QTC without routing |
| Capability | Salesforce Revenue Cloud | NetSuite (Native QTC) | SAP S/4HANA (Native QTC) | Zuora (Billing-Centric) |
|---|---|---|---|---|
| CPQ | Native or managed package | Native (limited) or add-on | SAP CPQ (separate product) | Partner CPQ only |
| Order Management | Native Order object | Native Sales Order | Native Sales Order + Delivery | Orders API (subscriptions only) |
| Billing | Revenue Cloud Billing (native) | Native invoicing + SuiteBilling | SAP Billing (convergent) | Native (core strength) |
| Subscription Mgmt | Revenue Cloud or Zuora | SuiteBilling (limited) | SAP BRIM | Native (core strength) |
| Revenue Recognition | RevPro or Zuora Revenue | Advanced Revenue Mgmt (ARM) | SAP RAR | Zuora Revenue (RevPro) |
| API Style | REST + SOAP + Bulk + Events | REST + SOAP + RESTlet | OData v4 + BAPI + IDoc | REST + SOAP + Callouts |
| Middleware Ecosystem | MuleSoft (native), Boomi, Workato | Celigo (native), Boomi, Workato | SAP IS (native), MuleSoft | Workato, MuleSoft, Boomi |
| Strength | CRM-to-billing seamless | All-in-one for mid-market | Enterprise financials & SCM | Best-in-class subscriptions |
| Weakness | Revenue Cloud still maturing | Limited CPQ, subscription billing | Complex setup, expensive | No native CPQ or CRM |