> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.fluveo.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.fluveo.com/_mcp/server.

# Checkout Sessions

> **Status: `contracted_test`.** The six canonical Checkout Session operations
> are in the generated [API Reference](/api-reference). Only `sk_test_*` mode is
> promised. See [API coverage](/api-coverage).

A Checkout Session represents one customer's journey through the Fluveo-hosted
payment page. You create it server-side, redirect the customer to its `url`, and
retrieve it on return to confirm the outcome. For the end-to-end flow, see the
[Checkout overview](/checkout/overview).

## Create

`POST /v1/checkout/sessions`

| Field                           | Required | Notes                                                                                            |
| ------------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `mode`                          | yes      | `payment` (single charge). `subscription` / `setup` are not available yet.                       |
| `success_url`                   | yes      | Where to redirect after success. Include `{CHECKOUT_SESSION_ID}` to receive the id back.         |
| `cancel_url`                    | yes      | Where to redirect on cancellation.                                                               |
| `line_items`                    | yes      | `[{ price_data: { currency, unit_amount, product_data: { name } }, quantity }]`                  |
| `customer`                      | no       | Reuse an existing [customer](/resources/customers).                                              |
| `customer_email`                | no       | Used for the receipt.                                                                            |
| `payment_method_types`          | no       | Restrict the methods offered.                                                                    |
| `metadata`                      | no       | Up to 50 key/value pairs. They stay on the session and are not copied to the PaymentIntent.      |
| `payment_intent_data[metadata]` | no       | Metadata for the session's PaymentIntent. See [PaymentIntent metadata](#paymentintent-metadata). |
| `expires_at`                    | no       | Unix timestamp; min 30 minutes, max 24 hours.                                                    |

**`curl`**

```bash title="curl"
curl -s -X POST https://api.devfluveo.com/v1/checkout/sessions \
  -u "$FLUVEO_API_KEY:" -H "Content-Type: application/json" \
  -d '{
    "mode": "payment",
    "success_url": "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
    "cancel_url":  "https://example.com/cart",
    "line_items":  [{ "price_data": { "currency": "usd", "unit_amount": 4242, "product_data": { "name": "Demo Plan" } }, "quantity": 1 }]
  }'
```

## PaymentIntent metadata

Session `metadata` stays on the session. If you want metadata on the
PaymentIntent too, send it in `payment_intent_data[metadata]`. Each rule below
that fails is a `400 invalid_request_error`, and nothing is created:

* At most **31 keys**: Stripe allows 50, and Fluveo and the payment processor
  reserve up to 19 keys on the PaymentIntent. Later PaymentIntent updates keep
  the same limit. A PaymentIntent that already has more than 31 keys can change
  or remove keys, but an update can never leave it with more keys than it had.
* No `order_id` key, in any letter case: the payment processor uses it to
  match the payment.
* No `invoice` key: it marks a hosted invoice payment, and only Fluveo's own
  hosted invoice page sets it.
* No empty key, and no key with `[` or `]`. The usual `metadata` rules apply
  too: no `fluveo_` prefix, keys up to 40 and values up to 500 characters.
* Session `metadata[order_id]` and `metadata[invoice]` are still fine, because
  they stay on the session.

## The session object

```json
{
  "id":             "cs_R9k8AzB2xQRH9Jf",
  "object":         "checkout.session",
  "mode":           "payment",
  "url":            "https://pay.fluveo.com/c/cs_R9k8AzB2xQRH9Jf",
  "status":         "open",
  "payment_status": "unpaid",
  "amount_total":   4242,
  "currency":       "usd",
  "success_url":    "https://example.com/thanks",
  "cancel_url":     "https://example.com/cart",
  "expires_at":     1769498745,
  "created":        1769412345,
  "livemode":       false
}
```

* `status`: `open` → `complete` (or `expired`).
* `payment_status`: `unpaid` → `paid`.

## Retrieve (confirm the outcome)

`GET /v1/checkout/sessions/{id}` — call this when the customer returns to your
`success_url`. **Fulfill only when `payment_status` is `paid`** — don't trust the
redirect by itself. Merchant-public Events and WebhookEndpoints are currently
unavailable, so re-retrieve the session from your server until it reaches a
terminal state; do not make fulfillment depend on a public webhook.

**`curl`**

```bash title="curl"
curl -s https://api.devfluveo.com/v1/checkout/sessions/cs_… -u "$FLUVEO_API_KEY:"
```

## Hidden from the public API

Each item below is hidden because it does not follow the Stripe parity ADR (ADR 0004 as amended). The [hidden public `/v1` registry](https://github.com/fluveohq/fluveo/blob/main/tests/api-contract/stripe/hidden-public-v1.json) lists every
hidden item. Public `/v1` answers a hidden operation as an unknown URL, refuses a hidden request
parameter or value with Stripe's `invalid_request_error`, and omits a hidden response key.

* the value `square` of `branding_settings[border_style]` on `POST /v1/checkout/sessions` (HIDE0-014)
* the value `hosted` of `ui_mode` on `POST /v1/checkout/sessions` (HIDE0-015)
* the request parameter `customer_email` of `POST /v1/checkout/sessions/{session}` (HIDE0-074)
* the request parameter `shipping_details` of `POST /v1/checkout/sessions/{session}` (HIDE0-075)