docs
IntegrateCheckout Session

Line items & pricing

How `lineItems` work on a Checkout Session: existing prices vs inline `priceData`, fixed and adjustable quantity, custom-amount lines, currency, and the availability gate.

A lineItems array on POST /checkout/sessions lists what the customer is paying for. Each item carries a price reference, a quantity, and optional rules for the customer to adjust quantity at checkout. The session aggregates them into the totals shown on the hosted page (amountSubtotal, amountTotal), and the same per-line shape is what the Checkout Session response carries back to you.

Two things drive every line item: which PriceAPI it points at, and how many of that price the customer is charged for. Everything else (currency, availability, presentment FX) flows from the price.

Existing prices vs inline priceData

A line item references a price one of two ways. Provide exactly one. Sending both, or neither, is rejected.

FieldWhen to use
priceThe PriceAPI's price_* ID. Use when the SKU exists in your catalog.
priceDataAd-hoc product and price created on the fly for this session. Use for one-off invoices, custom orders, donation pages.
// Catalog reference
{ "price": "price_test_AbC123", "quantity": 1 }

// Inline ad-hoc price
{
  "priceData": {
    "currency": "EGP",
    "unitAmount": 149900,
    "productData": { "name": "Custom order #4231" }
  },
  "quantity": 1
}

Inline prices are stored as one-off rows attached to the session. They don't enter your product catalog and don't show up in dashboard product lists. If the same item shows up across many sessions, create a real product and price once and reference it by ID. You'll get inventory tracking, analytics, and faster session creation.

priceData.unitAmount is in minor units. 149900 means 1,499.00 EGP. The full priceData shape:

FieldRequiredNotes
currencyyesISO 4217. Must match the session's customer-facing currency (see Currency below).
unitAmountyesPer-unit amount in minor units. Multiplied by quantity to produce the line subtotal.
productData.nameyesDisplay name shown in the order summary on the hosted page.
productData.descriptionnoSubline shown beneath the product name.
productData.imagenoPublic image URL. Fetched and stored server-side.
productData.unitLabelnoCustom label for "qty 2" displays (e.g. "seat", "license").
productData.metadatanoYour own key-value bag, opaque to XPay.

Quantity and adjustable quantity

quantity is required and must be at least 1. For most line items it's a fixed integer the merchant sets. For lines where the customer should be able to change quantity at checkout, set adjustableQuantity.

{
  "price": "price_test_AbC123",
  "quantity": 1,
  "adjustableQuantity": {
    "enabled": true,
    "minimum": 1,
    "maximum": 10
  }
}

When adjustableQuantity.enabled is true, the hosted page renders a / + stepper next to that line. The stepper respects minimum (default 1) and maximum (default 99). It also respects the price's remaining stock: if the price has finite stock, the stepper caps at whatever's left, and a "Only N left" hint appears below the stepper when stock is at or under 10.

The hosted page handles the change end-to-end. When the customer adjusts quantity, the totals update on the page and the session is updated server-side. You don't write any code for this path. If you want to change line items from your own backend, use PATCH /checkout/sessions/:id (full lineItems replacement, see Overview → What's mutable).

Currency and multi-currency

Every Price has a native currency. Within a single session, every line item must share that currency: mixing EGP and USD lines on the same session is rejected at create time.

XPay processes settlements in EGP today. When the price is in EGP, that single currency runs end to end. When the price is in another currency (the presentment currency), the session locks an exchange rate at creation and persists a parallel "what the customer sees" view alongside the EGP processing values.

Concretely:

  • The session and each line item carry two amount tracks in the response: the processing track (amountSubtotal, amountTotal, in EGP minor units) and a presentment-mirror sidecar (presentmentDetails.amountSubtotal, presentmentDetails.amountTotal, in the customer's currency).
  • The exchange rate is locked at session creation. Quantity changes, discounts, and updates all reuse it. The customer never sees the rate move during checkout.
  • The hosted checkout app reads the presentment values when they exist and renders the entire page in one currency. Your server reads the processing values for ledger and reconciliation.

You don't need to do FX math on either side. The session's response carries every value already projected.

Price availability

Every price has lifecycle fields that gate whether a line item can run at checkout. The same gate applies at session creation, on every quantity update, and again when the customer submits payment. A line item that flips to unavailable mid-session disables the pay button on the hosted page and surfaces a per-line badge.

StateCauseWhat the customer sees
archivedThe price was archived in the dashboard (active: false)."Unavailable" badge, line dimmed.
scheduledThe price has a startDate in the future."Not yet available" badge.
expiredThe price has an expirationDate that has passed."Expired" badge.
sold_outFinite stock is less than the requested quantity."Sold out" badge.
recurring_unsupportedThe price's type is RECURRING. Recurring prices aren't accepted in checkout."Unsupported price type" badge.
availableNone of the above.Normal render.

Stock is decremented atomically when the payment succeeds, not when the session is created. A session that opens with stock available can sell out by the time the customer pays. The gate at submit time catches that race and rejects the payment with a price_sold_out error rather than overselling.

stock of null (the default) means unlimited. Inline priceData lines never carry a stock cap.

Custom-amount lines

When a price has type: CUSTOM, the customer enters the amount themselves at checkout. Use this for donations, "pay what you want" pricing, or invoices where the merchant only knows the amount in the dashboard a moment before sending the link.

A custom-amount price carries a customUnitAmount configuration:

{
  "id": "price_test_donate",
  "type": "CUSTOM",
  "currency": "EGP",
  "customUnitAmount": {
    "minimum": 5000,    // 50.00 EGP
    "maximum": 10000000, // 100,000.00 EGP
    "preset": 50000      // 500.00 EGP shown by default
  }
}

A line item that resolves to a CUSTOM price seeds the session amount from customUnitAmount.preset (or 0 if no preset is set). The hosted page renders a large amount with a Change amount button that opens an input field. When the customer commits, the hosted page validates the entered value against the bounds and the session totals refresh in place.

CUSTOM lines come with a few hard rules:

  • At most one CUSTOM line per session, and it must be the only line item. Mixing it with other items is rejected.
  • quantity is forced to 1. Adjustable quantity is not allowed.
  • Discounts and promotion codes are not supported on sessions that contain a CUSTOM line. The whole point of the line is that the customer chose the amount; layering a coupon on top breaks the contract.
  • Inline priceData cannot be CUSTOM. Only existing price_* references created in the dashboard or via the Prices API can resolve to CUSTOM. The bounds (customUnitAmount) are configured on the Price, not on the line item.
  • uiMode: "custom" (Elements SDK) is incompatible. Build the session with a fixed unitAmount instead, or use uiMode: "hosted" / "embedded", both of which use the hosted checkout's amount-entry UI.

What's on each line item in the response

Every line item in the Checkout Session response carries the same fields:

FieldWhat it is
idLine item ID (li_*).
quantityResolved quantity (1 for CUSTOM lines, or whatever the customer set via the stepper).
priceThe full PriceAPI object, including its product, type (ONE_TIME / CUSTOM), customUnitAmount (if any), and lifecycle fields (active, stock, startDate, expirationDate).
adjustableQuantityEchoed back when set on creation. Drives the hosted page's stepper rendering.
amountSubtotalunitAmount × quantity, in the session's processing currency.
amountTotalAfter any discount or tax allocated to this line.
amountDiscountAllocated discount for this line. 0 when no discount applies. Sum across lines equals the session's discount.
amountTaxAllocated tax for this line. 0 when no tax applies.
currencyCurrency of the amount* fields above. On a Checkout Session this is the processing currency.
presentmentDetailsSidecar with unitAmount, amountSubtotal, amountDiscount, amountTotal, and currency in the customer's currency. Present only when the session is multi-currency.

Where to next

On this page