Guides

Working with product configurations (CPQ)

How to configure products through the Merchant API, from opening a configuration session to committing it and adding it to the cart.

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 NotAvailable.

    Note: To activate CPQ for your account, contact Geins support at geins-support@litium.com.

  • The shopper must be signed in (the Registered role), and their company must have an ERP customer number. If the shopper is signed out, or the company has no customer number, you get MissingCustomerNumber.
  • Pass the same channelId, marketId and languageId on 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 with ConfigurationCurrencyMismatch.

Note: Every call on this page is also available in the Geins SDK, through geinsOMS.configuration in @geins/oms. See Configuration (CPQ) in the SDK docs.

Three ids

IdWhere it comes fromLifetimeUsed for
configurationId (session)createConfiguration / reopenCartItemConfigurationUntil expiresAtgetConfiguration, applyConfigurationChanges, commitConfiguration, renewConfiguration, deleteConfiguration
committedConfigurationIdcommitConfigurationOutlives the session; price frozen at commitThe configurationId on a cart item
cart item idThe cartAs long as the cart line existsupdateCartItem, reopenCartItemConfiguration

A cart item's configurationId is always the committed id, never the session id. Carts live longer than sessions.

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 CpqConfigurationType (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, optionGroups) → optionGroups → 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 sortIndex, with nulls last. If every sortIndex is null, keep the order the API returned.
  • Visibility: hide a section when visible: false. Disable a variable, option group or option when available: false.
  • Read-only: a variable with readOnly: true is computed by the ERP. An option with readOnly: true has a fixed selection. Changes to either are refused, so render them as non-editable. (A LOCK you set yourself doesn't make an option read-only.)
  • Option groups: minSelections and maxSelections give the selection rules (maxSelections: 1 means single-select). quantityEditable and minQuantity/maxQuantity say whether, and how far, quantities can change.
  • Variables: valueType says what value holds (string, number or boolean; a date is a string). Numbers also come with min, max, step, decimals and unit.
  • Messages: can appear on any node. Show them by severity.
  • Option prices: option.unitPrice is the ERP price that the cart charges. option.product is 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 isValid: 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 } }
}
typeRequiredOptional
VARIABLEvariableId, value—
OPTIONoptionId, plus at least one of selected, quantity or lockinstanceId (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
QUANTITYquantity (how many of the configured product)—

Session lifetime: watch expiresAt and call renewConfiguration(configurationId) while the shopper is still active. When the shopper leaves, call deleteConfiguration. A commit ends the session: any later call with its configurationId returns ConfigurationGone (message "Configuration expired"), so keep the committedConfigurationId 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 ConfigurationMismatch.
  • If the market's currency differs from the session's, commitConfiguration still returns the committedConfigurationId, but without a price and with a ConfigurationCurrencyMismatch error. The configuration stays priced in the session's currency: addToCart accepts it only in a market that sells in that currency, and refuses it with ConfigurationCurrencyMismatch anywhere else.
  • A configured line never merges with another row. Adding the same committedConfigurationId twice gives two rows, so change a line's quantity with updateCartItem instead.
  • cartItem.configuration is copied onto the line when it's added, so reading the cart doesn't call CPQ. Use summary for a compact line display and sections for the full breakdown.

5. Edit a configured cart line

  1. Call reopenCartItemConfiguration(cartId, itemId) to get a new session at the line's quantity and today's price.
  2. Apply changes, then call commitConfiguration.
  3. Call updateCartItem(id: cartId, item: { id: <cart item id>, quantity, configurationId: <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, not skuId. Addressing by SKU is refused when several rows share the SKU.
  • If you get ConfigurationNotReopenable, the shopper has to configure the product again from scratch.
  • Sending configurationId: "" removes the configuration from the line. Leaving it out keeps it unchanged.
  • If the new configuration is refused, updateCartItem still returns data: the cart, unchanged, alongside the error. Always check errors, not just data. (addToCart fails the whole call instead.)

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 QUANTITY change.
  • configuration on an order row is only filled by getOrder and getOrderPublic; getOrders and getCompanyOrders return null. Order rows carry no configurationId, so a reorder means configuring again.

Error codes

Read extensions.code on the GraphQL error.

CodeMeaningWhat to do
NotAvailableCPQ isn't enabled on the accountHide configurator entry points
MissingCustomerNumberThe shopper is signed out, or their company has no ERP customer numberSign the shopper in. If they are signed in, it's an account setup issue
LoginRequiredA 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 companySign the shopper in
InvalidProductReference / ProductNotFoundBad articleNumber/productId on createFix the input
ConfigurationGoneThe session expired, was deleted, or was already committed (message "Configuration expired")Start a new session. After a commit, use the committedConfigurationId
ConfigurationNotFoundThe id never existedStart a new session
ConfigurationMismatchThe committed configuration's article ≠ the cart line's SKUUse the SKU of the configured article
ConfigurationCurrencyMismatchThe session or commit is priced in another currency than the marketOpen a new session in this market
CartItemNotConfiguredreopenCartItemConfiguration was called on an unconfigured line—
ConfigurationNotReopenableThe committed configuration can't be revisedConfigure again
ConfigurationFailedAny other CPQ failure, such as a refused change. Also returned by addToCart/updateCartItem with a configurationId when CPQ isn't enabledDon't retry the same request: a refused change fails the same way every time. Show the message and let the shopper change the input