Commerce and CPQ
The path from a product to a signed number: the policy, the profile that decides how a line is priced, price books, purchase costs, and quote versions.
Every endpoint here needs the cpq module. The flow runs in one direction: the workspace’s policy and profile decide what is possible, price books and purchase costs supply the numbers, and a quote version freezes a result.
The policy comes first
GET /settings/commercial answers the arithmetic every quote obeys. It is one object per workspace, carrying a revision, and nothing in a quote overrides it.
| Setting | Default | What it decides |
|---|---|---|
baseCurrency |
JPY |
The currency amounts are reported in |
currencies |
{} |
Which other currencies are allowed, and on what terms |
currencyScale |
0 |
Decimal places on a money amount, 0 to 4. The default matches the default currency |
rounding |
half_up |
How a computed price is rounded: half_up, half_even, toward_zero, floor, ceil |
taxRounding |
floor |
The same choice, applied to tax independently |
taxCalculation |
per_rate_total |
Whether tax is computed per_line or once per rate across the quote |
calculationOrder |
discount_before_tax |
Whether a discount lands before or after tax |
pricesIncludeTax |
false |
Whether a catalogue price is tax-inclusive |
fiscalYearStartMonth |
4 |
The reporting year’s first month |
timezone |
Asia/Tokyo |
The zone dates are interpreted in |
Changing the policy is a versioned write. PUT takes the revision it was read at and refuses a stale one with settings_stale. Quotes already written keep the policy they were priced under — each version carries a policySnapshot.
The profile decides how a line may be priced
GET /cpq/configuration answers one field, profile, and it decides which of five pricing methods a quote line may use.
| Method | catalog |
procurement |
hybrid |
Needs a purchase cost |
|---|---|---|---|---|
catalog |
✓ | ✓ | ||
cost_plus |
✓ | ✓ | ✓ | |
gross_margin |
✓ | ✓ | ✓ | |
resale |
✓ | ✓ | ✓ | |
manual |
✓ | ✓ |
A catalog workspace prices only from its price books. A procurement workspace cannot use catalog at all. Read the profile before offering a method in a user interface; a method the profile does not include is refused rather than ignored.
How each method computes a price
Rates are decimal fractions, not percentages, and every result is rounded with the policy’s rounding.
| Method | The arithmetic |
|---|---|
catalog |
The price book entry. Nothing is computed |
cost_plus |
cost × (1 + markup) |
gross_margin |
cost ÷ (1 − margin) |
resale |
retailPrice, then × (1 − rate) once per entry in downstreamMargins, which holds one to ten rates |
manual |
The unitPrice given. It also requires a reason of 1 to 500 characters |
The three cost-based methods refuse with purchase_cost_required when the cost entry is missing or negative. manual reports no cost and no profit; the others report unitProfit as unitPrice − unitCost.
manual is the escape hatch, and it is recorded as one. The required reason is stored on the line, so a quote priced by hand says why.
Products, price books and agreements
A product is something sellable. A price book is a named list of prices in one currency; prices enter it through POST /price-books/{id}/entries in batches of 1 to 500 rather than one at a time.
A price agreement binds one account to one price book for a period. When several apply to a line, the highest priority wins and the larger minimum quantity breaks a tie. A tie on both is refused with price_agreement_ambiguous rather than resolved arbitrarily — two agreements that disagree are a configuration problem, not a pricing question.
A purchase cost is what the business paid, recorded by POST /purchase-costs and read per product by GET /products/{id}/purchase-costs. A line that prices from one names the entry by costEntryId, so the quote records which historical cost it used rather than today’s.
A quote, its versions and its lines
A quote has a number, an account, optionally an opportunity, and a backing CRM record. It holds no amounts itself — those live on its versions.
A version is one priced state of the quote. It carries currency, subtotal, taxableAmount, tax, discount, total, the cpqProfile and policySnapshot it was priced under, a customerSnapshot, a contentHash, and a state from draft, submitted, approved, rejected, accepted and superseded.
The commerce endpoints report that state and do not set it; what moves a version through it is the approvals side, submitted against the quote’s record. What commerce does control is whether a version is frozen, which is a separate flag from the state.
A version’s lines carry quantity, unit, unitPrice, unitCost, netAmount, discountAmount, taxRate, the pricingMethod used, a priceSnapshot, and the sourcePriceEntryId or sourceCostEntryId the number came from. Two enumerations group them: chargeType is one_time, recurring or milestone, and componentKind is product, hardware, software, service, option or package.
Each line may carry schedules — how its amount is billed over time, with the same three kinds as chargeType, an interval in months, and a milestone label. They are a billing concern rather than a pricing one; the reference has their shape.
Drafts and revisions
Two paths exist, and they are not the same.
POST /quotes creates a quote and freezes its first version. POST /quotes/drafts creates the same thing with that version left unfrozen, which PUT /quotes/{id}/draft then edits in place. A frozen version cannot be edited — quote_version_frozen says to create a revision instead.
POST /quotes/{id}/versions revises a quote by adding a version. It refuses in two ways worth knowing:
quote_stale— the revision you sent is not the current one. Reload and try again.quote_draft_open— the latest version is not frozen yet. Finish the draft before revising. A draft and a revision are two ways to change the same quote, and only one can be in flight.quote_approval_pending— an approval is open on the quote. Withdraw it before revising.
POST /quotes/preview prices a quote without creating anything, which is how a user interface shows a total while someone is still typing.
Trying it without a cost history
A procurement or hybrid workspace cannot price a cost_plus line until a purchase cost exists for that product. Record one with POST /purchase-costs first, or use manual with a reason while the cost history is still being loaded.