CPQ API and Automation
This skill covers all programmatic CPQ operations driven through SBQQ.ServiceRouter — the single correct entry point for creating, pricing, amending, and renewing CPQ quotes without using the CPQ UI. Activate when Apex code, a batch job, an integration, or an external system needs to drive CPQ operations that must respect the pricing engine.
Before Starting
Gather this context before working on anything in this domain:
- Confirm CPQ managed package is installed. All API classes live in the
SBQQ namespace provided by the CPQ managed package. If SBQQ.ServiceRouter is not accessible, the package is not installed or the running user lacks the CPQ permission set.
- Know the operation type. Each CPQ operation maps to a specific loader string. Using the wrong loader string produces a runtime error or silently returns an empty model. Confirm whether the task is read, product-add, calculate, save, amend, or renew before writing any code.
- Count the quote lines. Synchronous quote calculation degrades noticeably past approximately 100 lines and can hit CPU governor limits in complex orgs. For high line-count quotes, plan to use the async calculate path or Large Quote Mode.
- Never use direct DML on SBQQ objects for pricing fields. Direct
insert or update on SBQQ__QuoteLine__c bypasses the CPQ pricing engine. Fields such as SBQQ__NetPrice__c, SBQQ__CustomerPrice__c, SBQQ__Discount__c, and SBQQ__RegularTotal__c will be stale or inconsistent until the next full recalculation — which may overwrite your changes or produce corrupt totals on the quote.
Core Concepts
SBQQ.ServiceRouter — The Single Entry Point
All CPQ programmatic operations flow through SBQQ.ServiceRouter. The class exposes two key static methods:
SBQQ.ServiceRouter.read(String loaderName, String uid) — reads a serialized JSON model for a given record.
SBQQ.ServiceRouter.save(String saverName, String model) — persists a serialized JSON model back to the database.
A separate method handles calculation:
SBQQ.ServiceRouter.calculateInBackground(String quoteId, SBQQ.CalculateCallback callback) — triggers async calculation and invokes the callback when complete.
The same operations are available via REST at POST /services/apexrest/SBQQ/ServiceRouter with a JSON body containing loaderName (or saverName) and model (a JSON-serialized string).
Loader Strings
Each operation maps to a specific loader string that the ServiceRouter dispatches on:
| Loader String |
Method |
Purpose |
QuoteReader |
read |
Read an existing quote and all its lines as a JSON model |
QuoteProductAdder |
save |
Add one or more products to a quote model |
QuoteCalculator |
save / async |
Re-price the quote model (sync or async) |
QuoteSaver |
save |
Persist a calculated quote model to the database |
ProductLoader |
read |
Load product catalog records into a quote-compatible product model |
ContractAmender |
read |
Create an amendment quote model from an approved contract |
ContractRenewer |
read |
Create a renewal quote model from an approved contract |
Passing an unrecognized string raises a runtime exception. Mixing read-phase loaders with save-phase savers (e.g., passing QuoteReader to save()) produces an error or a no-op.
Async Calculate API and SBQQ.CalculateCallback
The QuoteCalculator operation used synchronously inside ServiceRouter.save() runs the pricing engine in the same transaction. For quotes with many lines, this consumes CPU time that accumulates against governor limits. Salesforce CPQ also exposes an async calculate path:
- The calling code invokes
SBQQ.ServiceRouter.calculateInBackground(quoteId, callback).
- CPQ triggers a queueable calculation job outside the current transaction.
- When calculation completes, CPQ calls
callback.onCalculated(String quoteModel) on the class that implements SBQQ.CalculateCallback.
The callback class must be global and implement SBQQ.CalculateCallback, which requires a single method: void onCalculated(String quoteModel). Inside onCalculated, the implementation typically calls SBQQ.ServiceRouter.save('QuoteSaver', quoteModel) to persist the result.
Errors thrown inside onCalculated do not surface to the originating UI session. Robust implementations must log failures explicitly.
Why Direct DML Corrupts CPQ Totals
The CPQ pricing engine maintains a consistent financial model across related fields on SBQQ__QuoteLine__c and SBQQ__Quote__c. These fields are not independent — they are outputs of a multi-pass calculation that evaluates price rules, discount schedules, block pricing, contracted prices, and subscription math in sequence.
Direct update on a quote line field (e.g., setting SBQQ__Discount__c = 10) skips all pricing passes. CPQ does not automatically recalculate downstream fields (SBQQ__NetPrice__c, SBQQ__CustomerPrice__c, SBQQ__RegularTotal__c, SBQQ__Quote__c.SBQQ__NetTotal__c, etc.). The line record and the quote header become financially inconsistent. On the next save through the CPQ UI, CPQ may overwrite your discount value with the engine's own calculated result.
Use ServiceRouter with QuoteCalculator + QuoteSaver to apply any programmatic field change that affects pricing.
Common Patterns
Pattern 1: Programmatic Quote Creation with Products and Calculation
When to use: An external system, batch process, or automation needs to create a complete CPQ quote with products and accurate pricing without a user opening the CPQ UI.
How it works:
- Create a bare
SBQQ__Quote__c record with insert (header fields only — no line manipulation).
- Load the quote model:
String quoteModel = SBQQ.ServiceRouter.read('QuoteReader', quoteId);
- Load product records:
String productModel = SBQQ.ServiceRouter.read('ProductLoader', productId);
- Add products to the quote model:
String updatedModel = SBQQ.ServiceRouter.save('QuoteProductAdder', combineModels(quoteModel, productModel));
- Calculate pricing:
String calculatedModel = SBQQ.ServiceRouter.save('QuoteCalculator', updatedModel);
- Persist to database:
SBQQ.ServiceRouter.save('QuoteSaver', calculatedModel);
Why not direct insert of SBQQ__QuoteLine__c: Inserting lines directly bypasses the product configuration model, skips bundle expansion, and produces lines with no pricing engine state. The resulting lines will have null or zero values for all calculated price fields.
Pattern 2: Async Calculation for High Line-Count Quotes
When to use: The quote has more than approximately 100 lines, or the sync calculation path is hitting CPU governor limits or taking over 5 seconds.
How it works:
- Implement a
global class MyCalculateCallback implements SBQQ.CalculateCallback.
- In
onCalculated(String quoteModel), call SBQQ.ServiceRouter.save('QuoteSaver', quoteModel) and log any errors.
- Invoke:
SBQQ.ServiceRouter.calculateInBackground(quoteId, new MyCalculateCallback());
- The method returns immediately; calculation runs asynchronously as a queueable.
- Poll or use a platform event to detect completion if the calling process needs to wait.
Why not sync calculate: Synchronous QuoteCalculator via ServiceRouter.save() processes all lines in the current transaction. Past ~100 lines, CPU consumption can breach the 10-second limit, causing an unhandled LimitException that rolls back the entire transaction.
Pattern 3: Contract Amendment and Renewal via API
When to use: An integration or automation needs to programmatically create amendment or renewal quotes from approved contracts without a user clicking "Amend" or "Renew" in the UI.
How it works:
Amendment:
- Read the amendment model:
String amendModel = SBQQ.ServiceRouter.read('ContractAmender', contractId);
- Modify the returned model JSON to change quantities, add products, or end-date lines.
- Calculate:
String calculatedModel = SBQQ.ServiceRouter.save('QuoteCalculator', amendModel);
- Save:
SBQQ.ServiceRouter.save('QuoteSaver', calculatedModel);
Renewal:
- Read the renewal model:
String renewModel = SBQQ.ServiceRouter.read('ContractRenewer', contractId);
- Optionally modify the model (e.g., adjust quantities or pricing).
- Calculate and save as above.
The contract must have SBQQ__Status__c = 'Activated' (approved). Passing an unapproved contract ID returns an error or an empty model.
Decision Guidance
| Situation |
Recommended Approach |
Reason |
| Create a quote and add products programmatically |
QuoteReader → QuoteProductAdder → QuoteCalculator → QuoteSaver |
Follows the CPQ calculation pipeline in correct order |
| Re-price a quote after changing a field |
QuoteReader → modify model → QuoteCalculator → QuoteSaver |
Pricing engine must see the full model; partial updates are not supported |
| Quote has 100+ lines |
Async calculate via calculateInBackground + CalculateCallback |
Sync path risks CPU governor limit; async is the safe path |
| Amend an existing contract |
ContractAmender → QuoteCalculator → QuoteSaver |
ContractAmender sets up the amendment delta model correctly |
| Renew an expiring contract |
ContractRenewer → QuoteCalculator → QuoteSaver |
ContractRenewer clones subscriptions into a renewal quote |
| Add a product to a quote |
ProductLoader + QuoteProductAdder |
Load product into model format before adding to quote |
| Direct DML on SBQQ__QuoteLine__c for pricing fields |
Never — use ServiceRouter |
Direct DML bypasses pricing engine; produces corrupt totals |
| Call CPQ API from an external system |
REST POST /services/apexrest/SBQQ/ServiceRouter |
Same loaderName/model pattern over HTTP with OAuth session |
Recommended Workflow
- Identify the operation and select the loader string. Match the business requirement to the correct loader string from the taxonomy table. Confirm the input record (quote ID, contract ID, product ID) is available and the record is in the required state (e.g., contract must be activated for amendment/renewal).
- Read the current model. Call
SBQQ.ServiceRouter.read(loaderName, recordId) to obtain the JSON model. Do not construct the model JSON manually — the schema is internal to the CPQ package and changes across versions.
- Modify the model if required. For add-product flows, load the product model separately and pass both to
QuoteProductAdder. For field changes, parse the JSON, update the relevant fields, re-serialize, and pass to QuoteCalculator.
- Calculate pricing. Pass the modified model through
QuoteCalculator. For quotes under ~100 lines, use synchronous ServiceRouter.save('QuoteCalculator', model). For larger quotes, use calculateInBackground with a CalculateCallback implementation.
- Save the calculated model. Call
ServiceRouter.save('QuoteSaver', calculatedModel) to persist all CPQ fields. Do not save the model by DML — always use QuoteSaver.
- Validate the results. Query the saved
SBQQ__Quote__c and spot-check key calculated fields (SBQQ__NetTotal__c, SBQQ__GrossTotal__c, line-level SBQQ__NetPrice__c). Confirm the totals are non-null and match the expected pricing.
- Handle errors explicitly.
ServiceRouter calls can throw runtime exceptions. Wrap each call in try/catch, log failures with context (loaderName, record ID, model snippet), and surface errors to calling processes. In async callbacks, log failures to a custom object or platform event since exceptions in onCalculated are silent.
Review Checklist
Salesforce-Specific Gotchas
Direct DML on SBQQ__QuoteLine__c corrupts pricing totals — Writing directly to price fields (SBQQ__Discount__c, SBQQ__NetPrice__c, etc.) bypasses all pricing engine passes. The quote header totals become stale and the line data is financially inconsistent. The next UI-driven save may silently overwrite your values. Always route field changes through the ServiceRouter model pipeline.
Async calculate errors are silent — Exceptions thrown inside SBQQ.CalculateCallback.onCalculated() do not propagate to the original calling context. If the callback crashes, the quote is left in a partially calculated state with no user-visible error. Implement explicit logging (custom object insert, platform event) inside onCalculated error handlers.
Sync calculate degrades past ~100 lines — The synchronous QuoteCalculator path runs in the current Apex transaction. On complex orgs with many price rules or configuration rules, CPU consumption per line can be high. A 150-line quote can easily consume 8–9 seconds of CPU, leaving little headroom for the rest of the transaction. Plan the async path for any quote that could grow beyond 100 lines.
Model JSON schema is internal and version-specific — The JSON returned by QuoteReader, ContractAmender, etc. reflects the internal CPQ data model for the installed package version. Manually constructing or hardcoding model JSON is fragile — field names and nesting change across CPQ releases. Always start from a ServiceRouter.read() response and modify in place.
ContractAmender and ContractRenewer require Activated contracts — Passing a contract with SBQQ__Status__c other than 'Activated' returns an error or an empty model. This is not always obvious because the error message from ServiceRouter can be generic. Validate contract status before invoking these loaders.
Output Artifacts
| Artifact |
Description |
| Apex ServiceRouter invocation |
Code calling SBQQ.ServiceRouter.read() and save() with correct loader strings |
| CalculateCallback class |
global class implementing SBQQ.CalculateCallback for async calculation flows |
| REST call example |
POST body shape for /services/apexrest/SBQQ/ServiceRouter |
| Loader string decision table |
Mapping from operation to loaderName/saverName |
| Amendment/renewal workflow |
Apex sequence for contract amendment or renewal via ContractAmender/ContractRenewer |
Related Skills
apex/cpq-apex-plugins — CPQ plugin interfaces (QuoteCalculatorPlugin, OrderPlugin) for hooking into calculation lifecycle; distinct from ServiceRouter-based API operations
admin/cpq-pricing-rules — Declarative price rules that fire during quote calculation; understand these before using the API to override prices
admin/cpq-quote-templates — Quote template configuration; required to understand how quote output relates to programmatic quote data
1---2name: cpq-api-and-automation3description: Use when programmatically driving Salesforce CPQ operations from Apex or REST — creating quotes, adding products, calculating pricing, saving quotes, amending contracts, or renewing contracts through the SBQQ.ServiceRouter API. Trigger keywords: CPQ API, SBQQ.ServiceRouter, QuoteCalculator, QuoteReader, QuoteSaver, QuoteProductAdder, ProductLoader, ContractAmender, ContractRenewer, programmatic quote, calculate callback, CPQ REST API. NOT for CPQ plugin interfaces — use apex/cpq-apex-plugins. NOT for testing CPQ from Apex — use apex/cpq-test-automation.4---56# CPQ API and Automation78This skill covers all programmatic CPQ operations driven through `SBQQ.ServiceRouter` — the single correct entry point for creating, pricing, amending, and renewing CPQ quotes without using the CPQ UI. Activate when Apex code, a batch job, an integration, or an external system needs to drive CPQ operations that must respect the pricing engine.910---1112## Before Starting1314Gather this context before working on anything in this domain:1516- **Confirm CPQ managed package is installed.** All API classes live in the `SBQQ` namespace provided by the CPQ managed package. If `SBQQ.ServiceRouter` is not accessible, the package is not installed or the running user lacks the CPQ permission set.17- **Know the operation type.** Each CPQ operation maps to a specific loader string. Using the wrong loader string produces a runtime error or silently returns an empty model. Confirm whether the task is read, product-add, calculate, save, amend, or renew before writing any code.18- **Count the quote lines.** Synchronous quote calculation degrades noticeably past approximately 100 lines and can hit CPU governor limits in complex orgs. For high line-count quotes, plan to use the async calculate path or Large Quote Mode.19- **Never use direct DML on SBQQ objects for pricing fields.** Direct `insert` or `update` on `SBQQ__QuoteLine__c` bypasses the CPQ pricing engine. Fields such as `SBQQ__NetPrice__c`, `SBQQ__CustomerPrice__c`, `SBQQ__Discount__c`, and `SBQQ__RegularTotal__c` will be stale or inconsistent until the next full recalculation — which may overwrite your changes or produce corrupt totals on the quote.2021---2223## Core Concepts2425### SBQQ.ServiceRouter — The Single Entry Point2627All CPQ programmatic operations flow through `SBQQ.ServiceRouter`. The class exposes two key static methods:2829- `SBQQ.ServiceRouter.read(String loaderName, String uid)` — reads a serialized JSON model for a given record.30- `SBQQ.ServiceRouter.save(String saverName, String model)` — persists a serialized JSON model back to the database.3132A separate method handles calculation:3334- `SBQQ.ServiceRouter.calculateInBackground(String quoteId, SBQQ.CalculateCallback callback)` — triggers async calculation and invokes the callback when complete.3536The same operations are available via REST at `POST /services/apexrest/SBQQ/ServiceRouter` with a JSON body containing `loaderName` (or `saverName`) and `model` (a JSON-serialized string).3738### Loader Strings3940Each operation maps to a specific loader string that the `ServiceRouter` dispatches on:4142| Loader String | Method | Purpose |43|---|---|---|44| `QuoteReader` | `read` | Read an existing quote and all its lines as a JSON model |45| `QuoteProductAdder` | `save` | Add one or more products to a quote model |46| `QuoteCalculator` | `save` / async | Re-price the quote model (sync or async) |47| `QuoteSaver` | `save` | Persist a calculated quote model to the database |48| `ProductLoader` | `read` | Load product catalog records into a quote-compatible product model |49| `ContractAmender` | `read` | Create an amendment quote model from an approved contract |50| `ContractRenewer` | `read` | Create a renewal quote model from an approved contract |5152Passing an unrecognized string raises a runtime exception. Mixing read-phase loaders with save-phase savers (e.g., passing `QuoteReader` to `save()`) produces an error or a no-op.5354### Async Calculate API and SBQQ.CalculateCallback5556The `QuoteCalculator` operation used synchronously inside `ServiceRouter.save()` runs the pricing engine in the same transaction. For quotes with many lines, this consumes CPU time that accumulates against governor limits. Salesforce CPQ also exposes an **async calculate path**:57581. The calling code invokes `SBQQ.ServiceRouter.calculateInBackground(quoteId, callback)`.592. CPQ triggers a queueable calculation job outside the current transaction.603. When calculation completes, CPQ calls `callback.onCalculated(String quoteModel)` on the class that implements `SBQQ.CalculateCallback`.6162The callback class must be `global` and implement `SBQQ.CalculateCallback`, which requires a single method: `void onCalculated(String quoteModel)`. Inside `onCalculated`, the implementation typically calls `SBQQ.ServiceRouter.save('QuoteSaver', quoteModel)` to persist the result.6364Errors thrown inside `onCalculated` do not surface to the originating UI session. Robust implementations must log failures explicitly.6566### Why Direct DML Corrupts CPQ Totals6768The CPQ pricing engine maintains a consistent financial model across related fields on `SBQQ__QuoteLine__c` and `SBQQ__Quote__c`. These fields are not independent — they are outputs of a multi-pass calculation that evaluates price rules, discount schedules, block pricing, contracted prices, and subscription math in sequence.6970Direct `update` on a quote line field (e.g., setting `SBQQ__Discount__c = 10`) skips all pricing passes. CPQ does not automatically recalculate downstream fields (`SBQQ__NetPrice__c`, `SBQQ__CustomerPrice__c`, `SBQQ__RegularTotal__c`, `SBQQ__Quote__c.SBQQ__NetTotal__c`, etc.). The line record and the quote header become financially inconsistent. On the next save through the CPQ UI, CPQ may overwrite your discount value with the engine's own calculated result.7172Use `ServiceRouter` with `QuoteCalculator` + `QuoteSaver` to apply any programmatic field change that affects pricing.7374---7576## Common Patterns7778### Pattern 1: Programmatic Quote Creation with Products and Calculation7980**When to use:** An external system, batch process, or automation needs to create a complete CPQ quote with products and accurate pricing without a user opening the CPQ UI.8182**How it works:**83841. Create a bare `SBQQ__Quote__c` record with `insert` (header fields only — no line manipulation).852. Load the quote model: `String quoteModel = SBQQ.ServiceRouter.read('QuoteReader', quoteId);`863. Load product records: `String productModel = SBQQ.ServiceRouter.read('ProductLoader', productId);`874. Add products to the quote model: `String updatedModel = SBQQ.ServiceRouter.save('QuoteProductAdder', combineModels(quoteModel, productModel));`885. Calculate pricing: `String calculatedModel = SBQQ.ServiceRouter.save('QuoteCalculator', updatedModel);`896. Persist to database: `SBQQ.ServiceRouter.save('QuoteSaver', calculatedModel);`9091**Why not direct insert of SBQQ__QuoteLine__c:** Inserting lines directly bypasses the product configuration model, skips bundle expansion, and produces lines with no pricing engine state. The resulting lines will have null or zero values for all calculated price fields.9293### Pattern 2: Async Calculation for High Line-Count Quotes9495**When to use:** The quote has more than approximately 100 lines, or the sync calculation path is hitting CPU governor limits or taking over 5 seconds.9697**How it works:**98991. Implement a `global class MyCalculateCallback implements SBQQ.CalculateCallback`.1002. In `onCalculated(String quoteModel)`, call `SBQQ.ServiceRouter.save('QuoteSaver', quoteModel)` and log any errors.1013. Invoke: `SBQQ.ServiceRouter.calculateInBackground(quoteId, new MyCalculateCallback());`1024. The method returns immediately; calculation runs asynchronously as a queueable.1035. Poll or use a platform event to detect completion if the calling process needs to wait.104105**Why not sync calculate:** Synchronous `QuoteCalculator` via `ServiceRouter.save()` processes all lines in the current transaction. Past ~100 lines, CPU consumption can breach the 10-second limit, causing an unhandled `LimitException` that rolls back the entire transaction.106107### Pattern 3: Contract Amendment and Renewal via API108109**When to use:** An integration or automation needs to programmatically create amendment or renewal quotes from approved contracts without a user clicking "Amend" or "Renew" in the UI.110111**How it works:**112113Amendment:1141. Read the amendment model: `String amendModel = SBQQ.ServiceRouter.read('ContractAmender', contractId);`1152. Modify the returned model JSON to change quantities, add products, or end-date lines.1163. Calculate: `String calculatedModel = SBQQ.ServiceRouter.save('QuoteCalculator', amendModel);`1174. Save: `SBQQ.ServiceRouter.save('QuoteSaver', calculatedModel);`118119Renewal:1201. Read the renewal model: `String renewModel = SBQQ.ServiceRouter.read('ContractRenewer', contractId);`1212. Optionally modify the model (e.g., adjust quantities or pricing).1223. Calculate and save as above.123124The contract must have `SBQQ__Status__c = 'Activated'` (approved). Passing an unapproved contract ID returns an error or an empty model.125126---127128## Decision Guidance129130| Situation | Recommended Approach | Reason |131|---|---|---|132| Create a quote and add products programmatically | QuoteReader → QuoteProductAdder → QuoteCalculator → QuoteSaver | Follows the CPQ calculation pipeline in correct order |133| Re-price a quote after changing a field | QuoteReader → modify model → QuoteCalculator → QuoteSaver | Pricing engine must see the full model; partial updates are not supported |134| Quote has 100+ lines | Async calculate via calculateInBackground + CalculateCallback | Sync path risks CPU governor limit; async is the safe path |135| Amend an existing contract | ContractAmender → QuoteCalculator → QuoteSaver | ContractAmender sets up the amendment delta model correctly |136| Renew an expiring contract | ContractRenewer → QuoteCalculator → QuoteSaver | ContractRenewer clones subscriptions into a renewal quote |137| Add a product to a quote | ProductLoader + QuoteProductAdder | Load product into model format before adding to quote |138| Direct DML on SBQQ__QuoteLine__c for pricing fields | Never — use ServiceRouter | Direct DML bypasses pricing engine; produces corrupt totals |139| Call CPQ API from an external system | REST POST /services/apexrest/SBQQ/ServiceRouter | Same loaderName/model pattern over HTTP with OAuth session |140141---142143## Recommended Workflow1441451. **Identify the operation and select the loader string.** Match the business requirement to the correct loader string from the taxonomy table. Confirm the input record (quote ID, contract ID, product ID) is available and the record is in the required state (e.g., contract must be activated for amendment/renewal).1462. **Read the current model.** Call `SBQQ.ServiceRouter.read(loaderName, recordId)` to obtain the JSON model. Do not construct the model JSON manually — the schema is internal to the CPQ package and changes across versions.1473. **Modify the model if required.** For add-product flows, load the product model separately and pass both to `QuoteProductAdder`. For field changes, parse the JSON, update the relevant fields, re-serialize, and pass to `QuoteCalculator`.1484. **Calculate pricing.** Pass the modified model through `QuoteCalculator`. For quotes under ~100 lines, use synchronous `ServiceRouter.save('QuoteCalculator', model)`. For larger quotes, use `calculateInBackground` with a `CalculateCallback` implementation.1495. **Save the calculated model.** Call `ServiceRouter.save('QuoteSaver', calculatedModel)` to persist all CPQ fields. Do not save the model by DML — always use `QuoteSaver`.1506. **Validate the results.** Query the saved `SBQQ__Quote__c` and spot-check key calculated fields (`SBQQ__NetTotal__c`, `SBQQ__GrossTotal__c`, line-level `SBQQ__NetPrice__c`). Confirm the totals are non-null and match the expected pricing.1517. **Handle errors explicitly.** `ServiceRouter` calls can throw runtime exceptions. Wrap each call in try/catch, log failures with context (loaderName, record ID, model snippet), and surface errors to calling processes. In async callbacks, log failures to a custom object or platform event since exceptions in `onCalculated` are silent.152153---154155## Review Checklist156157- [ ] All CPQ operations go through `SBQQ.ServiceRouter` — no direct DML on SBQQ pricing fields158- [ ] Loader strings match the intended operation (read vs. save vs. async)159- [ ] Contract is in Activated status before calling ContractAmender or ContractRenewer160- [ ] Quote line count evaluated — async path used if count may exceed ~100 lines161- [ ] `SBQQ.CalculateCallback` implementation is `global` and handles errors explicitly162- [ ] REST calls include valid OAuth token and correct Content-Type: application/json163- [ ] Calculated model saved via `QuoteSaver`, not via DML164- [ ] Governor limits (CPU, heap, SOQL) reviewed in debug logs under realistic line counts165- [ ] Errors from ServiceRouter calls are caught and logged with sufficient context166167---168169## Salesforce-Specific Gotchas1701711. **Direct DML on SBQQ__QuoteLine__c corrupts pricing totals** — Writing directly to price fields (`SBQQ__Discount__c`, `SBQQ__NetPrice__c`, etc.) bypasses all pricing engine passes. The quote header totals become stale and the line data is financially inconsistent. The next UI-driven save may silently overwrite your values. Always route field changes through the ServiceRouter model pipeline.1721732. **Async calculate errors are silent** — Exceptions thrown inside `SBQQ.CalculateCallback.onCalculated()` do not propagate to the original calling context. If the callback crashes, the quote is left in a partially calculated state with no user-visible error. Implement explicit logging (custom object insert, platform event) inside `onCalculated` error handlers.1741753. **Sync calculate degrades past ~100 lines** — The synchronous `QuoteCalculator` path runs in the current Apex transaction. On complex orgs with many price rules or configuration rules, CPU consumption per line can be high. A 150-line quote can easily consume 8–9 seconds of CPU, leaving little headroom for the rest of the transaction. Plan the async path for any quote that could grow beyond 100 lines.1761774. **Model JSON schema is internal and version-specific** — The JSON returned by `QuoteReader`, `ContractAmender`, etc. reflects the internal CPQ data model for the installed package version. Manually constructing or hardcoding model JSON is fragile — field names and nesting change across CPQ releases. Always start from a `ServiceRouter.read()` response and modify in place.1781795. **ContractAmender and ContractRenewer require Activated contracts** — Passing a contract with `SBQQ__Status__c` other than `'Activated'` returns an error or an empty model. This is not always obvious because the error message from ServiceRouter can be generic. Validate contract status before invoking these loaders.180181---182183## Output Artifacts184185| Artifact | Description |186|---|---|187| Apex ServiceRouter invocation | Code calling `SBQQ.ServiceRouter.read()` and `save()` with correct loader strings |188| CalculateCallback class | `global class` implementing `SBQQ.CalculateCallback` for async calculation flows |189| REST call example | POST body shape for `/services/apexrest/SBQQ/ServiceRouter` |190| Loader string decision table | Mapping from operation to loaderName/saverName |191| Amendment/renewal workflow | Apex sequence for contract amendment or renewal via ContractAmender/ContractRenewer |192193---194195## Related Skills196197- `apex/cpq-apex-plugins` — CPQ plugin interfaces (QuoteCalculatorPlugin, OrderPlugin) for hooking into calculation lifecycle; distinct from ServiceRouter-based API operations198- `admin/cpq-pricing-rules` — Declarative price rules that fire during quote calculation; understand these before using the API to override prices199- `admin/cpq-quote-templates` — Quote template configuration; required to understand how quote output relates to programmatic quote data