# Line items & pricing (/en/integrate/checkout-session/line-items-and-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 <ApiLink href="/api-reference/objects/price">Price</ApiLink> 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` [#existing-prices-vs-inline-pricedata]

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

| Field       | When to use                                                                                                               |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| `price`     | The <ApiLink href="/api-reference/objects/price">Price</ApiLink>'s `price_*` ID. Use when the SKU exists in your catalog. |
| `priceData` | Ad-hoc product and price created on the fly for this session. Use for one-off invoices, custom orders, donation pages.    |

```jsonc
// 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:

| Field                     | Required | Notes                                                                                                             |
| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `currency`                | yes      | ISO 4217. Must match the session's customer-facing currency (see [Currency](#currency-and-multi-currency) below). |
| `unitAmount`              | yes      | Per-unit amount in minor units. Multiplied by `quantity` to produce the line subtotal.                            |
| `productData.name`        | yes      | Display name shown in the order summary on the hosted page.                                                       |
| `productData.description` | no       | Subline shown beneath the product name.                                                                           |
| `productData.image`       | no       | Public image URL. Fetched and stored server-side.                                                                 |
| `productData.unitLabel`   | no       | Custom label for "qty 2" displays (e.g. "seat", "license").                                                       |
| `productData.metadata`    | no       | Your own key-value bag, opaque to XPay.                                                                           |

## Quantity and adjustable quantity [#quantity-and-adjustable-quantity]

A line item's role is derived from two fields: its `quantity` and its `adjustableQuantity`. There's no separate type flag. The combination decides whether the customer must buy the line, can remove it, or starts without it.

| Role                | Starting `quantity` | `adjustableQuantity`                   | What the customer sees                                                             |
| ------------------- | ------------------- | -------------------------------------- | ---------------------------------------------------------------------------------- |
| **Required**        | 1 or more           | Off, or on with `minimum` of 1 or more | A fixed line they can't remove. A stepper appears only when adjustable.            |
| **Removable**       | 1 or more           | On, `minimum` 0                        | Starts in the order. They can lower it to 0 to drop it.                            |
| **Optional add-on** | 0                   | On, `minimum` 0                        | Starts outside the order, listed under "Add to your order" with an **Add** button. |

The common case is a fixed line: set `quantity` and leave `adjustableQuantity` off.

```jsonc
{ "price": "price_test_AbC123", "quantity": 1 }
```

To let the customer change the count at checkout, turn on `adjustableQuantity`:

```jsonc
{
  "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.

### Removable and optional items [#removable-and-optional-items]

Set `adjustableQuantity.minimum` to `0` to let the customer take the line all the way out of the order.

* **Removable.** Start `quantity` at 1 or more with `minimum: 0`. The line sits in the order from the start, and at quantity 1 the `−` button becomes a remove button that drops it to 0.
* **Optional add-on.** Start `quantity` at `0` with `minimum: 0`. The line starts outside the order, listed under an "Add to your order" section. The customer brings it in at quantity 1 with the **Add** button.

```jsonc
// Optional add-on: starts out of the cart, customer can add up to 3
{
  "price": "price_test_giftwrap",
  "quantity": 0,
  "adjustableQuantity": { "enabled": true, "minimum": 0, "maximum": 3 },
}
```

A `quantity` of `0` is valid only on a line where `adjustableQuantity.enabled` is `true` and `minimum` is `0`. Any other line sent with `quantity: 0` is rejected at create time.

You can make every line optional. The session is created with a total of 0 and an empty order, and the customer adds at least one item before paying. The hosted page keeps the pay button disabled while the order is empty, and a payment attempt with nothing in the order is rejected with `checkout_empty_cart`.

The hosted page handles every quantity change end-to-end: the totals update on the page and the session updates on the server. You don't write any code for this path. To change line items from your own backend, use `PATCH /checkout/sessions/:id` (full `lineItems` replacement, see [Overview → What's mutable](/integrate/checkout-session/overview#what-s-mutable)).

## Currency and multi-currency [#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 [#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.

| State                   | Cause                                                                            | What the customer sees            |
| ----------------------- | -------------------------------------------------------------------------------- | --------------------------------- |
| `archived`              | The price was archived in the dashboard (`active: false`).                       | "Unavailable" badge, line dimmed. |
| `scheduled`             | The price has a `startDate` in the future.                                       | "Not yet available" badge.        |
| `expired`               | The price has an `expirationDate` that has passed.                               | "Expired" badge.                  |
| `sold_out`              | Finite `stock` is less than the requested quantity.                              | "Sold out" badge.                 |
| `recurring_unsupported` | The price's `type` is `RECURRING`. Recurring prices aren't accepted in checkout. | "Unsupported price type" badge.   |
| `available`             | None 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.

An optional line sitting at `quantity` 0 isn't being purchased, so the availability gate skips it: a sold-out add-on never blocks payment for the rest of the order. It's only gated once the customer adds it and the quantity goes above 0.

## Custom-amount lines [#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:

```jsonc
{
  "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 [#whats-on-each-line-item-in-the-response]

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

| Field                | What it is                                                                                                                                                                                                                               |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | Line item ID (`li_*`).                                                                                                                                                                                                                   |
| `quantity`           | Resolved quantity. `1` for CUSTOM lines; `0` for an optional add-on the customer hasn't added; otherwise the line's quantity, either the merchant's fixed value or whatever the customer set via the stepper on an adjustable line.      |
| `price`              | The full <ApiLink href="/api-reference/objects/price">Price</ApiLink> object, including its product, type (`ONE_TIME` / `CUSTOM`), `customUnitAmount` (if any), and lifecycle fields (`active`, `stock`, `startDate`, `expirationDate`). |
| `adjustableQuantity` | Echoed back when set on creation. Drives the hosted page's stepper rendering.                                                                                                                                                            |
| `amountSubtotal`     | `unitAmount × quantity`, in the session's processing currency.                                                                                                                                                                           |
| `amountTotal`        | After any discount or tax allocated to this line.                                                                                                                                                                                        |
| `amountDiscount`     | Allocated discount for this line. `0` when no discount applies. Sum across lines equals the session's discount.                                                                                                                          |
| `amountTax`          | Allocated tax for this line. `0` when no tax applies.                                                                                                                                                                                    |
| `currency`           | Currency of the `amount*` fields above. On a Checkout Session this is the processing currency.                                                                                                                                           |
| `presentmentDetails` | Sidecar with `unitAmount`, `amountSubtotal`, `amountDiscount`, `amountTotal`, and `currency` in the customer's currency. Present only when the session is multi-currency.                                                                |

Optional items the customer never adds aren't part of a completed order. Once payment succeeds, the completed `GET /checkout/sessions/:id` and the `checkout.session.completed` event carry only the lines the customer bought. A line left at `quantity` 0 is dropped from both.

## Where to next [#where-to-next]

<Cards>
  <Card icon="<ShoppingCart />" title="Customer lifecycle" href="/integrate/checkout-session/customer-lifecycle">
    Decide who the customer is, what fields the form collects, and how the customer record is
    treated.
  </Card>

  <Card icon="<Receipt />" title="After completion" href="/integrate/checkout-session/after-completion">
    Configure where the customer lands after payment.
  </Card>

  <Card icon="<SquareStack />" title="Advanced configuration" href="/integrate/checkout-session/advanced-configuration">
    Branding, locale, payment methods, fees, and metadata.
  </Card>
</Cards>