# Customer lifecycle (/en/integrate/checkout-session/customer-lifecycle)

Decide who the customer is, what the form collects, and how XPay's customer record evolves across one-time payers and returning customers.

A <ApiLink href="/api-reference/objects/customer">Customer</ApiLink> is XPay's record of someone who paid you. It carries their name, email, phone, address, and the lifetime spend tied to those identifiers. This page is about how that record gets created, prefilled, and re-used across the three integration scenarios you're likely to build: returning authenticated customers, first-time authenticated customers, and guest checkouts.

The single rule that drives every decision below: &#x2A;*store the `cus_*` against your authenticated user, never against a guest.** XPay deduplicates guest checkouts server-side; if you pin a `cus_*` to a guest in your database you'll fight that dedup.

## The two customer types [#the-two-customer-types]

XPay tracks two kinds of Customer records.

| Type           | When it's created                                                                                                                                                                                                                                                                    | Use                                                                                                                                |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Registered** | Either you set `customerCreation: "always"` on a Checkout Session and the customer pays, or the merchant explicitly attached a `customerId` you already had. The dashboard counts them as known customers; you can attach saved payment methods to them later (subscriptions, etc.). | Your authenticated user paid. Save the `cus_*` to their account row.                                                               |
| **Guest**      | Auto-created during a checkout when no `customerId` is provided and `customerCreation` is `if_required` (the default). XPay groups guests by email, phone, and card fingerprint so the same person is recognized across visits.                                                      | A one-time payer or someone who hasn't signed in. &#x2A;*Don't store the `cus_*`**: XPay maintains the dedup, you'd just fight it. |

Both types appear in the dashboard. The guest's grouping fields (every email and phone they've used, every card they've paid with) build up automatically. The dashboard also surfaces "Related customers" so an operator can see when a guest record overlaps with a registered one.

## Three patterns by merchant scenario [#three-patterns-by-merchant-scenario]

The shape of `customerCreation`, `customerId`, and `customerDetails` you send depends on what you know about the person paying.

### Returning authenticated user [#returning-authenticated-user]

You have a `cus_*` stored on your user row. Pass it as `customerId`. The form prefills from the existing record. Don't pass `customerDetails` (it's mutually exclusive with `customerId`). Optionally set `customerUpdate` to write the form's changes back.

<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}" } },
        "customerId": "cus_test_AbC123",
        "customerUpdate": { "address": "auto", "shipping": "auto" },
        "lineItems": [ { "price": "price_test_xyz", "quantity": 1 } ]
      }'
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript
    const session = 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}" },
        },
        customerId: user.xpay_customer_id, // stored on your user row
        customerUpdate: { address: "auto", shipping: "auto" },
        lineItems: [{ price: "price_test_xyz", quantity: 1 }],
      }),
    }).then((r) => r.json());
    ```
  </Tab>

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

    session = 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}"},
            },
            "customerId": user.xpay_customer_id,  # stored on your user row
            "customerUpdate": {"address": "auto", "shipping": "auto"},
            "lineItems": [{"price": "price_test_xyz", "quantity": 1}],
        },
        timeout=10,
    ).json()
    ```
  </Tab>
</Tabs>

### First-time authenticated user [#first-time-authenticated-user]

No `cus_*` yet. Send what you know as `customerDetails` for prefill, and set `customerCreation: "always"` so a registered Customer is created at payment time. Capture `data.object.customer.id` from the `checkout.session.completed` webhook and store it on your user row.

<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}" } },
        "customerCreation": "always",
        "customerDetails": {
          "name": "Aya Hassan",
          "email": "aya@example.com",
          "phone": "+201234567890"
        },
        "metadata": { "user_id": "u_42" },
        "lineItems": [ { "price": "price_test_xyz", "quantity": 1 } ]
      }'
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript
    const session = 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}" },
        },
        customerCreation: "always",
        customerDetails: {
          name: user.name,
          email: user.email,
          phone: user.phone,
        },
        metadata: { user_id: user.id },
        lineItems: [{ price: "price_test_xyz", quantity: 1 }],
      }),
    }).then((r) => r.json());
    ```
  </Tab>

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

    session = 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}"},
            },
            "customerCreation": "always",
            "customerDetails": {
                "name": user.name,
                "email": user.email,
                "phone": user.phone,
            },
            "metadata": {"user_id": user.id},
            "lineItems": [{"price": "price_test_xyz", "quantity": 1}],
        },
        timeout=10,
    ).json()
    ```
  </Tab>
</Tabs>

### Guest checkout [#guest-checkout]

No authenticated user. Omit `customerId` and leave `customerCreation` as the default (`if_required`). XPay creates or matches a guest record by email, phone, and card fingerprint. You don't need to track the `cus_*` on your side. The dashboard groups guests automatically.

<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": [ { "price": "price_test_xyz", "quantity": 1 } ]
      }'
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript
    const session = 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: [{ price: "price_test_xyz", quantity: 1 }],
      }),
    }).then((r) => r.json());
    ```
  </Tab>

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

    session = 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": [{"price": "price_test_xyz", "quantity": 1}],
        },
        timeout=10,
    ).json()
    ```
  </Tab>
</Tabs>

The webhook that fires on success (`checkout.session.completed`) carries the resolved Customer in `data.object.customer`. For the first-time-authenticated case, that's where you grab the new `cus_*` to save on your user row.

## `customerId` vs `customerDetails` [#customerid-vs-customerdetails]

These are the two mutually-exclusive ways to identify the customer on a session.

| Field             | What it does                                                                                                                                                        | Required type |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `customerId`      | References an existing Customer (`cus_*`). The form prefills name, email, phone, billing address, and shipping from that record.                                    | Registered    |
| `customerDetails` | Inline contact and address data (`name`, `email`, `phone`, `billingDetails.address`, `shipping`). Prefills the form. Used to create a new Customer at payment time. | n/a           |

Sending both is rejected. Sending neither is fine: the form is blank and the customer fills it in.

`customerId` must point at a **registered** Customer. Passing a guest's `cus_*` fails with a clear error. Guests are checkout-only; there's no path to attach them to a future session.

### Prefill rules [#prefill-rules]

When the page renders, the form is pre-filled in priority order:

1. `customerDetails.{name, email, phone}` if provided.
2. The existing Customer's `name`, `email`, `phone`, `address`, `shipping` if `customerId` was set.
3. Empty otherwise. The customer fills it in.

The email and phone fields are **locked** (read-only) when `customerId` is set and the existing record carries them. The customer can't change someone else's email or phone mid-checkout.

## `customerCreation` [#customercreation]

Controls when XPay creates a Customer record at payment time. Two values; default is `if_required`.

| Value         | What it does                                                                                                                                                                                                                |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `if_required` | Default. If the customer fills in an email or phone, a **guest** Customer is created or matched. If they fill in nothing and the merchant didn't set `customerId`, no Customer record is created at all.                    |
| `always`      | A **registered** Customer is created from `customerDetails` at payment time, even if they only typed a name. Use this when you want a known account on file (returning visits, post-purchase emails, future subscriptions). |

`customerCreation` is mutually exclusive with `customerId: "cus_..."` for `always`. If you already have a Customer, you're not asking XPay to create another one.

## `customerUpdate` [#customerupdate]

When `customerId` is set, the form-collected data is **not** written back to the existing Customer by default. Set `customerUpdate` to opt-in per field. Each sub-flag is `auto` (write) or `never` (default, don't write).

```jsonc
{
  "customerId": "cus_test_AbC123",
  "customerUpdate": {
    "name": "auto", // overwrite customer.name with the form value
    "address": "auto", // overwrite customer.address with the billing address
    "shipping": "auto", // overwrite customer.shipping with the shipping address
  },
}
```

`customerUpdate` is only meaningful when `customerId` is provided. Sending it without `customerId` is a validation error.

`email` and `phone` aren't in `customerUpdate` because they're locked at the form level when the existing customer carries them. They can only be **backfilled** when missing (see [How XPay handles guest customers](#how-xpay-handles-guest-customers)).

## What the form collects [#what-the-form-collects]

Four boolean toggles control which fields the hosted checkout form asks the customer to fill. All default to `false`.

| Toggle                      | What it asks for                                                | Required at submit?                                                                     |
| --------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `nameCollection`            | Customer's full name.                                           | Yes when on.                                                                            |
| `phoneNumberCollection`     | Customer's phone with country code.                             | Yes when on. Submit fails with `parameter_missing` on `customerDetails.phone` if blank. |
| `billingAddressCollection`  | Cardholder name and full billing address (street, city, state). | Yes when on. Submit fails on missing `customerDetails.billingDetails.address.line1`.    |
| `shippingAddressCollection` | Recipient name and shipping address.                            | Yes when on. Submit fails on missing `customerDetails.shipping.address.line1`.          |

Email is always asked for and always required. It's the receipt destination and the primary identifier for guest dedup.

The submit button label is set separately by `submitType` (`pay` / `subscribe` / `book` / `donate`).

### Custom fields [#custom-fields]

Need to ask for a Tax ID, an order note, or a delivery preference? `customFields` is an array of up to 3 entries. Each one renders an input on the form and the customer's answer is returned back on the session response and the webhook payload.

| Field type | Input rendered                                  | Validation                                                                |
| ---------- | ----------------------------------------------- | ------------------------------------------------------------------------- |
| `TEXT`     | Single-line text input.                         | Optional `minCharacters` / `maxCharacters` via `hasLimits` + `limitType`. |
| `NUMBER`   | Number-only input.                              | Same character limits apply.                                              |
| `DROPDOWN` | Select with merchant-defined `dropdownOptions`. | At least 2 options required.                                              |
| `CHECKBOX` | Single checkbox the customer ticks.             | No character limits.                                                      |

```jsonc
{
  "customFields": [
    {
      "label": "Tax ID",
      "type": "TEXT",
      "isOptional": false,
      "hasLimits": true,
      "limitType": "BETWEEN",
      "minCharacters": 9,
      "maxCharacters": 15,
    },
    {
      "label": "Delivery preference",
      "type": "DROPDOWN",
      "dropdownOptions": [{ "label": "Leave at door" }, { "label": "Hand to recipient" }],
    },
  ],
}
```

Each field gets a stable `key` server-side (derived from `label`) so the answer comes back at the same key in the response payload. The collected value lives in the matching `text` / `numeric` / `dropdown` slot on each entry.

## How XPay handles guest customers [#how-xpay-handles-guest-customers]

The default checkout flow (no `customerId`, `customerCreation: "if_required"`) creates a guest Customer the first time someone checks out. &#x2A;*The same guest is matched on subsequent visits when any identifier overlaps.** You don't write any code for this.

XPay matches by:

* **Email**, case-insensitive and trimmed.
* **Phone**, normalized (country code, `00` prefix converted to `+`).
* **Card fingerprint**, captured after a successful charge. If a guest pays with a card that's been seen on another guest record, XPay merges the two and consolidates spend, payment history, and devices on the survivor.

What this gives you in the dashboard:

* A guest's profile compounds every email, phone, and card they've used over time.
* A guest's spend, refunds, and payment count are accurate even when they checked out three times with three slightly different forms.
* The "Related customers" section on a registered customer's profile surfaces the guest records that share contact info with them, so an operator can correlate the two.

What guest customers can't do:

* They **can't have a saved payment method attached for future charges**. Every guest checkout is a fresh card entry. If your product needs saved cards (subscriptions, one-click reordering), you must set `customerCreation: "always"` to create a registered Customer instead.

## Avoiding duplicate customer records [#avoiding-duplicate-customer-records]

The biggest failure mode merchants hit: re-creating an XPay customer on every login or every order, instead of re-using the one you already have. The pattern below avoids that.

<Steps>
  <Step>
    ### Store the `cus_*` against your authenticated user [#store-the-cus_-against-your-authenticated-user]

    When a logged-in user makes their first payment, set `customerCreation: "always"` and capture `data.object.customer.id` from the `checkout.session.completed` webhook. Save that `cus_*` on your user row in your database.

    ```typescript
    // In your webhook handler
    if (event.type === "checkout.session.completed") {
      const session = event.data.object;
      const userId = session.metadata?.user_id; // you set this on session create
      const customerId = session.customer?.id; // cus_test_...

      if (userId && customerId) {
        await db.users.update(userId, { xpay_customer_id: customerId });
      }
    }
    ```
  </Step>

  <Step>
    ### Pass it as `customerId` on every subsequent session [#pass-it-as-customerid-on-every-subsequent-session]

    For every following payment by that same user, set `customerId: "cus_test_..."`. The form prefills, no new Customer is created, and the spend rolls up under the same record.

    ```typescript
    const session = 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: returnUrl } },
        customerId: user.xpay_customer_id, // re-use, don't recreate
        lineItems: [...],
      }),
    });
    ```
  </Step>

  <Step>
    ### Don't store guest `cus_*` on user rows [#dont-store-guest-cus_-on-user-rows]

    If a user paid as a guest first (before signing up) and then created an account, the right move is to start the `cus_*` story on their first **registered** payment. Don't try to back-fill the guest's `cus_*` onto their account. Guests dedup server-side; an operator can see the connection in the dashboard's "Related customers" section.
  </Step>

  <Step>
    ### Use `metadata` to thread your own user ID [#use-metadata-to-thread-your-own-user-id]

    Set `metadata: { user_id: "..." }` on every session you create. It comes back on the webhook payload, on the session retrieve response, and on the Payment Intent. Makes your webhook handler trivial: the session payload tells you exactly which user paid.
  </Step>
</Steps>

The result: one XPay registered Customer per authenticated user, lifetime spend rolls up correctly, and guests are tracked separately without you having to manage them.

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

<Cards>
  <Card icon="<TimerReset />" title="After completion" href="/integrate/checkout-session/after-completion">
    Configure where the customer lands after they pay.
  </Card>

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

  <Card icon="<Webhook />" title="Set up a webhook endpoint" href="/integrate/webhooks/setting-up-an-endpoint">
    Receive `checkout.session.completed` so you can capture `data.object.customer.id`.
  </Card>
</Cards>