Working with product configurations (CPQ)
Configurable products are built in the ERP's configurator and reached through the Merchant API. The shopper opens a configuration session, makes choices, and commits it. The committed configuration then goes on a cart line. The ERP sets the price; the Merchant API adds VAT for the market.
Before you start
- CPQ must be enabled on the account. If it isn't, every configuration call returns the error code
Not.Available Note: To activate CPQ for your account, contact Geins support at geins-support@litium.com.
- The shopper must be signed in (the
Registeredrole), and their company must have an ERP customer number. If the shopper is signed out, or the company has no customer number, you getMissing.Customer Number - Pass the same
channel,Id marketandId languageon every call. The session's customer and currency are fixed when it's created. If a later call (or the cart) uses a market with another currency, it's refused withId Configuration.Currency Mismatch
Note: Every call on this page is also available in the Geins SDK, through
geinsinOMS.configuration @geins/oms. See Configuration (CPQ) in the SDK docs.
Three ids
| Id | Where it comes from | Lifetime | Used for |
|---|---|---|---|
configuration (session) | create / reopen | Until expires | get, apply, commit, renew, delete |
committed | commit | Outlives the session; price frozen at commit | The configuration on a cart item |
cart item id | The cart | As long as the cart line exists | update, reopen |
A cart item's
configurationis always the committed id, never the session id. Carts live longer than sessions.Id
Overview
flowchart LR
A[createConfiguration] --> B[applyConfigurationChanges]
B --> C[commitConfiguration]
C --> D[addToCart]
D --> E[Checkout]
Each step is described below. Steps 3 and 5 have their own diagrams.
1. Open a session
mutation {
createConfiguration(articleNumber: "E-108", quantity: 1) { # or productId: 1234 (give one, not both)
configurationId
expiresAt
isValid
quantity
unitPrice { sellingPriceIncVat sellingPriceExVat regularPriceIncVat isDiscounted }
messages { severity text }
sections { ...Section }
}
}
Every call that returns a Cpq (create, get, apply, reopen) returns the whole re-evaluated state. Replace what the client holds with it; don't merge.
2. Render the tree
The configuration is a tree: sections → (sections, variables, option) → option → options. GraphQL can't select recursively, so write your fragments as deep as your product models go:
fragment Option on CpqOptionType {
id instanceId articleNumber name selected available readOnly quantity minQuantity maxQuantity
unitPrice { sellingPriceIncVat }
product { productId name } # Geins catalogue product, often null
messages { severity text }
}
fragment Group on CpqOptionGroupType {
id name available sortIndex minSelections maxSelections quantityEditable
options { ...Option }
}
fragment Variable on CpqVariableType {
id name valueType value defaultValue required available readOnly min max step decimals unit sortIndex
messages { severity text }
}
fragment Section on CpqSectionType {
id name visible sortIndex
variables { ...Variable }
optionGroups { ...Group optionGroups { ...Group } }
sections { id name visible sortIndex variables { ...Variable } optionGroups { ...Group } }
}
This selects two levels of sections and two levels of option groups. Add levels if your products nest deeper.
- Order: within a parent, sort sections, variables and option groups together on
sort, with nulls last. If everyIndex sortis null, keep the order the API returned.Index - Visibility: hide a section when
visible: false. Disable a variable, option group or option whenavailable: false. - Read-only: a variable with
readis computed by the ERP. An option withOnly: true readhas a fixed selection. Changes to either are refused, so render them as non-editable. (AOnly: true LOCKyou set yourself doesn't make an option read-only.) - Option groups:
minandSelections maxgive the selection rules (Selections maxmeans single-select).Selections: 1 quantityandEditable min/Quantity maxsay whether, and how far, quantities can change.Quantity - Variables:
valuesays whatType valueholds (string, number or boolean; a date is a string). Numbers also come withmin,max,step,decimalsandunit. - Messages: can appear on any node. Show them by
severity. - Option prices:
option.unitis the ERP price that the cart charges.Price option.productis the matching Geins catalogue product (for images and copy). It is often null, and its own price is not the CPQ price. - Commit gate: enable "Add to cart" only when
is.Valid: true
3. Apply changes
Send changes as a batch. If one change fails, none are applied.
mutation {
applyConfigurationChanges(
configurationId: "cfg-123"
changes: [ # ids are placeholders: take them from the tree the session returned
{ type: VARIABLE, variableId: "width", value: 1200 }
{ type: OPTION, optionId: "colour-red", selected: true }
{ type: OPTION, optionId: "shelf", instanceId: "2", quantity: 3, lock: LOCK }
{ type: QUANTITY, quantity: 5 }
]
) { configurationId isValid unitPrice { sellingPriceIncVat } sections { ...Section } }
}
type | Required | Optional |
|---|---|---|
VARIABLE | variable, value | — |
OPTION | option, plus at least one of selected, quantity or lock | instance (which instance of a repeated option; leave it out for the first). lock is NONE / LOCK / UNLOCK; LOCK keeps the option from being changed by later rule cascades |
QUANTITY | quantity (how many of the configured product) | — |
Session lifetime: watch expires and call renew while the shopper is still active. When the shopper leaves, call delete. A commit ends the session: any later call with its configuration returns Configuration (message "Configuration expired"), so keep the committed instead.
flowchart TD
A[createConfiguration] --> B[Live session]
B --> C{isValid?}
C -->|no| D[applyConfigurationChanges]
D --> B
C -->|yes| E[commitConfiguration]
E --> F[Add to cart]
B -.->|expired, deleted or committed| G[ConfigurationGone]
G -.->|start over| A
4. Commit and add to cart
mutation {
commitConfiguration(configurationId: "cfg-123") {
committedConfigurationId
quantity
unitPrice { sellingPriceIncVat }
summary { label value }
}
}
mutation {
addToCart(id: "<cartId>", item: { skuId: 5678, quantity: 5, configurationId: "<committedConfigurationId>" }) {
items {
id
configurationId
configuration { articleNumber summary { label value } }
}
}
}
- Add the line at the committed
quantity. The committed price was quoted for that quantity (see Pricing). - The committed configuration must be for the same article as the SKU. Otherwise you get
Configuration.Mismatch - If the market's currency differs from the session's,
commitstill returns theConfiguration committed, but without a price and with aConfiguration Id Configurationerror. The configuration stays priced in the session's currency:Currency Mismatch addaccepts it only in a market that sells in that currency, and refuses it withTo Cart Configurationanywhere else.Currency Mismatch - A configured line never merges with another row. Adding the same
committedtwice gives two rows, so change a line's quantity withConfiguration Id updateinstead.Cart Item cartis copied onto the line when it's added, so reading the cart doesn't call CPQ. UseItem.configuration summaryfor a compact line display andsectionsfor the full breakdown.
5. Edit a configured cart line
- Call
reopento get a new session at the line's quantity and today's price.Cart Item Configuration(cart Id, item Id) - Apply changes, then call
commit.Configuration - Call
update.Cart Item(id: cart Id, item: { id: <cart item id>, quantity, configuration Id: <new committed id> })
Until step 3, the line keeps its old configuration.
sequenceDiagram
participant Client
participant API as Merchant API
Client->>API: reopenCartItemConfiguration(cartId, itemId)
API-->>Client: new session (configurationId)
Client->>API: applyConfigurationChanges(configurationId, changes)
API-->>Client: updated configuration
Client->>API: commitConfiguration(configurationId)
API-->>Client: committedConfigurationId
Client->>API: updateCartItem(item: id, quantity, configurationId)
API-->>Client: cart with the updated line
- Address configured lines by cart item
id, notsku. Addressing by SKU is refused when several rows share the SKU.Id - If you get
Configuration, the shopper has to configure the product again from scratch.Not Reopenable - Sending
configurationremoves the configuration from the line. Leaving it out keeps it unchanged.Id: "" - If the new configuration is refused,
updatestill returns data: the cart, unchanged, alongside the error. Always checkCart Item errors, not justdata. (addfails the whole call instead.)To Cart
6. Pricing and orders
- The cart asks CPQ to price configured lines at the cart's quantity. If CPQ can't answer, the line falls back to its committed unit price × quantity. That fallback doesn't apply ERP quantity breaks, which is why the cart quantity should match the committed one. To price the line at a new quantity, reopen it and send a
QUANTITYchange. configurationon an order row is only filled bygetandOrder get;Order Public getandOrders getreturn null. Order rows carry noCompany Orders configuration, so a reorder means configuring again.Id
Error codes
Read extensions.code on the GraphQL error.
| Code | Meaning | What to do |
|---|---|---|
Not | CPQ isn't enabled on the account | Hide configurator entry points |
Missing | The shopper is signed out, or their company has no ERP customer number | Sign the shopper in. If they are signed in, it's an account setup issue |
Login | A signed-out read of a cart that belongs to a company. On a CPQ account, a cart a company buyer has changed (for example, one with a configured line) belongs to that company | Sign the shopper in |
Invalid / Product | Bad article/product on create | Fix the input |
Configuration | The session expired, was deleted, or was already committed (message "Configuration expired") | Start a new session. After a commit, use the committed |
Configuration | The id never existed | Start a new session |
Configuration | The committed configuration's article ≠ the cart line's SKU | Use the SKU of the configured article |
Configuration | The session or commit is priced in another currency than the market | Open a new session in this market |
Cart | reopen was called on an unconfigured line | — |
Configuration | The committed configuration can't be revised | Configure again |
Configuration | Any other CPQ failure, such as a refused change. Also returned by add/update with a configuration when CPQ isn't enabled | Don't retry the same request: a refused change fails the same way every time. Show the message and let the shopper change the input |