# Overview (/en/integrate/checkout-session/overview)

The Checkout Session is the central object every integration creates. The same shape comes back from POST, GET, the SDK client, and every checkout.session webhook.

A Checkout Session represents one customer's attempt to pay you. Your server creates it, XPay returns the session object, and from there every integration pattern is just a different way to render the same object: a hosted page, a payment link, an SDK drop-in, or fully custom Elements.

The session object you get back from `POST /checkout/sessions`, the one you `GET` later, and the `data.object` carried on every <ApiLink href="/api-reference/objects/checkout-session">`checkout.session`</ApiLink> webhook are all the same shape. There is one DTO and one mapper behind all three. If you build against the create response, your webhook handler reads the same fields by the same names.

For how Checkout Session relates to Payment Intent, Charge, Refund, Customer, and Balance Transaction, see [Object model](/integrate/object-model).

## How it fits [#how-it-fits]

Pick the integration pattern that matches how much UI control you want. Each pattern guide spells out the build, the test flow, the webhook handler, and the production checklist for that surface. They all create a Checkout Session the same way.

<Cards>
  <Card icon="<LinkIcon />" title="Payment Links" href="/integrate/integration-patterns/payment-links">
    No-code. Share a `https://checkout.xpay.app/p/plink_...` URL. A session is created on the
    customer's first interaction.
  </Card>

  <Card icon="<ExternalLink />" title="Hosted Checkout" href="/integrate/integration-patterns/hosted-checkout">
    Server-only. Create a session, redirect the customer to `session.url`, confirm with a webhook.
  </Card>

  <Card icon="<AppWindow />" title="Drop-in" href="/integrate/integration-patterns/drop-in">
    Server plus a few lines of frontend. Open the checkout in a modal or inline iframe on your
    domain.
  </Card>

  <Card icon="<Component />" title="Elements" href="/integrate/integration-patterns/elements">
    Server plus custom UI. Build the form yourself with `<PaymentElement />` and `confirmPayment()`.
  </Card>
</Cards>

## Lifecycle [#lifecycle]

A session moves through two short state machines: `status` for the session itself and `paymentStatus` for the money.

### `status` [#status]

| State      | When it's set                                                                                                    | What you do                                                 |
| ---------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `open`     | Set at creation. The session accepts mutations and is renderable in the checkout UI.                             | Wait for the customer to pay, or for the session to expire. |
| `complete` | Set when payment succeeds. The session is locked: no more mutations, no more payment attempts.                   | Fulfill the order against `checkout.session.completed`.     |
| `expired`  | Set when `expiresAt` passes, or when you call `POST /checkout/sessions/:id/expire`. The session can't be reused. | Create a new session if the customer wants to try again.    |

### `paymentStatus` [#paymentstatus]

| State                 | When it's set                                                              |
| --------------------- | -------------------------------------------------------------------------- |
| `unpaid`              | Default. No successful charge has occurred.                                |
| `paid`                | A charge has succeeded for the session's full amount.                      |
| `no_payment_required` | The session was created in `setup` mode (collecting payment details only). |

The `paymentIntent` field on the response is `null` until the customer submits the form for the first time. After that, it carries the full <ApiLink href="/api-reference/objects/payment-intent">Payment Intent</ApiLink> shape, identical to what `GET /payment-intents/:id` returns. <ApiLink href="/api-reference/objects/charge">Charges</ApiLink> and <ApiLink href="/api-reference/objects/refund">Refunds</ApiLink> are nested inside the Payment Intent.

## Creating a session [#creating-a-session]

The only required field on `POST /checkout/sessions` is `afterCompletion`. Everything else has a default. In practice you'll also send `lineItems` so the customer sees what's being charged. Lines can be fixed, adjustable, or optional add-ons the customer opts into, and an all-optional session is valid: it opens at a total of 0 and the customer adds items before paying. See [Line items & pricing](/integrate/checkout-session/line-items-and-pricing).

<Tabs items="[&#x22;cURL&#x22;, &#x22;Node.js&#x22;, &#x22;Python&#x22;]">
  <Tab value="cURL">
    ```bash
    curl -X POST https://api.xpay.app/checkout/sessions \
      -H "Authorization: Bearer sk_test_..." \
      -H "Content-Type: application/json" \
      -d '{
        "afterCompletion": {
          "type": "redirect",
          "redirect": { "url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" }
        },
        "lineItems": [
          {
            "priceData": {
              "currency": "EGP",
              "unitAmount": 149900,
              "productData": { "name": "Test product" }
            },
            "quantity": 1
          }
        ]
      }'
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript
    const res = await fetch("https://api.xpay.app/checkout/sessions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        afterCompletion: {
          type: "redirect",
          redirect: { url: "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" },
        },
        lineItems: [
          {
            priceData: {
              currency: "EGP",
              unitAmount: 149900, // 1,499.00 EGP, in minor units
              productData: { name: "Test product" },
            },
            quantity: 1,
          },
        ],
      }),
    });

    const session = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os, requests

    res = requests.post(
        "https://api.xpay.app/checkout/sessions",
        headers={
            "Authorization": f"Bearer {os.environ['XPAY_SECRET_KEY']}",
            "Content-Type": "application/json",
        },
        json={
            "afterCompletion": {
                "type": "redirect",
                "redirect": {"url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}"},
            },
            "lineItems": [
                {
                    "priceData": {
                        "currency": "EGP",
                        "unitAmount": 149900,
                        "productData": {"name": "Test product"},
                    },
                    "quantity": 1,
                },
            ],
        },
        timeout=10,
    )

    session = res.json()
    ```
  </Tab>
</Tabs>

The response carries the full session, including the two credentials your integration will use next:

```json
{
  "id": "cs_test_AbC123...",
  "object": "checkout.session",
  "status": "open",
  "paymentStatus": "unpaid",
  "url": "https://checkout.xpay.app/c/cs_test_AbC123...",
  "clientSecret": "cs_test_AbC123..._secret_xyz",
  "amountTotal": 149900,
  "currency": "EGP",
  "afterCompletion": { "type": "redirect", "redirect": { "url": "..." } },
  "paymentIntent": null,
  "livemode": false
}
```

Use `url` to redirect the customer (Hosted Checkout, Payment Links). Use `clientSecret` to mount the SDK (Drop-in, Elements). The literal `{CHECKOUT_SESSION_ID}` token in `redirect.url` is replaced server-side with the session's `id` before the URL is stored, so your return page can read it off the path without you threading it through query parameters.

The full set of fields you can configure is grouped by purpose. Each group has its own page with the field-by-field detail.

<Cards>
  <Card icon="<ShoppingCart />" title="Line items & pricing" href="/integrate/checkout-session/line-items-and-pricing">
    Existing prices vs inline `priceData`, quantities, adjustable quantities, custom-amount lines,
    currency.
  </Card>

  <Card icon="<User />" title="Customer lifecycle" href="/integrate/checkout-session/customer-lifecycle">
    `customerId` vs `customerDetails`, `customerCreation`, `customerUpdate`, name, phone, address,
    shipping, custom fields.
  </Card>

  <Card icon="<TimerReset />" title="After completion" href="/integrate/checkout-session/after-completion">
    `redirect` vs `hosted_confirmation`, `cancelUrl`, return-button URL, custom message.
  </Card>

  <Card icon="<SlidersHorizontal />" title="Advanced configuration" href="/integrate/checkout-session/advanced-configuration">
    Branding, locale, payment-method types, fees and VAT pass-through, expiration, metadata.
  </Card>
</Cards>

## Modes [#modes]

Two enums on the session decide what kind of payment it represents and how the customer interacts with it. Both are set at creation and immutable for the rest of the session's life.

### `mode` [#mode]

`mode` is the kind of money movement. It defaults to `payment`.

| Value          | Meaning                                                                                          |
| -------------- | ------------------------------------------------------------------------------------------------ |
| `payment`      | One-time charge. The default for nearly every integration.                                       |
| `subscription` | Recurring billing. Not yet supported on the checkout API.                                        |
| `setup`        | Collect a payment method without charging it. `paymentStatus` resolves to `no_payment_required`. |

### `uiMode` [#uimode]

`uiMode` is how the customer reaches the form. It defaults to `hosted`. The choice is tied to the integration pattern you picked above; the session's behavior, the SDK call you make, and the fields you can set all change with it.

| Value      | Where the form runs                                                                      | Use it with                                                                                                                         |
| ---------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `hosted`   | XPay's hosted page at `https://checkout.xpay.app/c/cs_test_...`. Server returns a `url`. | [Hosted Checkout](/integrate/integration-patterns/hosted-checkout), [Payment Links](/integrate/integration-patterns/payment-links). |
| `embedded` | An iframe on your site, opened by the SDK as a modal or inline.                          | [Drop-in](/integrate/integration-patterns/drop-in).                                                                                 |
| `custom`   | Your own form, using the Elements SDK and `confirmPayment()`.                            | [Elements](/integrate/integration-patterns/elements).                                                                               |

A few rules fall out of this:

* `cancelUrl` is only valid when `uiMode: "hosted"`. It's the redirect XPay sends the customer to if a payment attempt fails on the hosted page (declined, 3DS rejected, local-method timeout). It is not a customer-cancel button: the hosted page does not have one.
* **`afterCompletion.type` must be `"redirect"` for `embedded` and `custom`.** `hosted_confirmation` renders an XPay-hosted page, and these integrations run on your own site. See [After completion](/integrate/checkout-session/after-completion#sdk-integrations-need-a-return-url).
* `uiMode: "custom"` means your code owns customer collection. The session rejects `nameCollection`, `phoneNumberCollection`, `billingAddressCollection`, `shippingAddressCollection`, and `submitType` in this mode.
* For `embedded` and `custom`, you authenticate the SDK with the session's `clientSecret`. The publishable key alone is not enough; the secret scopes the SDK to one specific session.

## Mutually exclusive fields [#mutually-exclusive-fields]

A handful of pairs on the create body are exclusive. The API rejects requests that send both, and the same rules apply on `PATCH`.

| Either               | Or                                 | Why                                                                                                                                                     |
| -------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customerId`         | `customerDetails`                  | An existing <ApiLink href="/api-reference/objects/customer">Customer</ApiLink>'s record is the source of prefill, or you provide raw details. Not both. |
| `customerId`         | `customerCreation: "always"`       | "Always create a new customer" contradicts "use this existing customer."                                                                                |
| `paymentMethodTypes` | `paymentMethodConfigurationId`     | Pick a literal list of methods, or reference a saved configuration.                                                                                     |
| `lineItem.price`     | `lineItem.priceData`               | Each line item references an existing <ApiLink href="/api-reference/objects/price">Price</ApiLink> by id, or it inlines product and price data.         |
| A `CUSTOM`-type line | `allowPromotionCodes`, `discounts` | Custom-amount lines (customer enters the amount) are not compatible with discounts.                                                                     |

`customerUpdate` (which controls whether checkout writes collected data back onto the customer) is only meaningful when `customerId` is provided. Sending it without `customerId` is a validation error.

## What's mutable [#whats-mutable]

`PATCH /checkout/sessions/:id` updates an open session. It accepts every field on the create body except the ones below, which are locked at creation:

* `mode`
* `uiMode`
* `submitType`
* `currency`
* `expiresAfterMinutes`

`lineItems` on `PATCH` is a full replacement: the new array overwrites the old one. To change a single item's quantity, send the entire desired array. Sessions in `complete` or `expired` status are read-only and reject `PATCH`.

## Expiration [#expiration]

Every session has an `expiresAt`. Default is 24 hours after creation; minimum is 30 minutes. Set `expiresAfterMinutes` on the create body to change it.

Two things mark a session as `expired`:

* `expiresAt` passes. The session is no longer usable for payment; the hosted page renders a terminal "you're all done here" state.
* You explicitly call `POST /checkout/sessions/:id/expire`. Useful when you want to stop accepting payment on a session you no longer intend to honor, or to free up redemption slots on a one-shot promotion code.

Both paths emit a `checkout.session.expired` webhook.

## Webhooks [#webhooks]

Two events are emitted for the session's own lifecycle. The `data.object` on each one is a full <ApiLink href="/api-reference/objects/checkout-session">Checkout Session</ApiLink>, identical in shape to what `GET /checkout/sessions/:id` returns.

| Event                        | When                                                                                                              |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `checkout.session.completed` | Payment succeeded. `status` is `complete`, `paymentStatus` is `paid` (or `no_payment_required` for `setup` mode). |
| `checkout.session.expired`   | The session expired by time or by an explicit `/expire` call.                                                     |

A successful payment on Hosted Checkout emits four events. They are dispatched in this sequence:

1. `payment_intent.created`
2. `charge.succeeded`
3. `payment_intent.succeeded`
4. `checkout.session.completed`

Webhook delivery is asynchronous and runs with retries, so **do not rely on receipt order** in your handler. Key fulfillment off `checkout.session.completed` and treat the earlier events as supplementary context. The earlier ones are useful when you need to react to the underlying <ApiLink href="/api-reference/objects/payment-intent">Payment Intent</ApiLink> or <ApiLink href="/api-reference/objects/charge">Charge</ApiLink> directly. See the [Event reference](/integrate/webhooks/event-reference) for the full list.

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

<Cards>
  <Card icon="<Network />" title="Object model" href="/integrate/object-model">
    How Checkout Session relates to Payment Intent, Charge, Customer, and Refund. The IDs to keep on
    your order record.
  </Card>

  <Card icon="<ShoppingCart />" title="Line items & pricing" href="/integrate/checkout-session/line-items-and-pricing">
    Build the cart: existing prices, inline `priceData`, quantities, custom amounts.
  </Card>

  <Card icon="<User />" title="Customer lifecycle" href="/integrate/checkout-session/customer-lifecycle">
    Decide how XPay treats the customer and which fields the form collects.
  </Card>

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

  <Card icon="<Webhook />" title="Webhooks: setting up an endpoint" href="/integrate/webhooks/setting-up-an-endpoint">
    Receive `checkout.session.completed` and verify the signature.
  </Card>
</Cards>