---
title: "Hosted checkout"
description: "Send the buyer to a Fluveo page that takes the payment."
url: https://docs.fluveo.com/payments/checkout/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/payments/checkout.curl.md (cURL), https://docs.fluveo.com/payments/checkout.node.md (Node.js), https://docs.fluveo.com/payments/checkout.python.md (Python), https://docs.fluveo.com/payments/checkout.php.md (PHP).

# Hosted checkout

Send the buyer to a Fluveo page that takes the payment.

With hosted checkout you skip the payment form. Your server describes the cart in one call, and the answer is a Checkout Session: a page on Fluveo's domain with its own address. You send the buyer to that address. The page shows the cart and its total, collects the payment, and returns the buyer to your shop. No card number ever reaches your servers. Each session pays through a PaymentIntent, so [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md) holds for it too.

Pick it when a page you host is enough and you would rather not build a form. Checkout [platforms](https://docs.fluveo.com/platforms.md) can point their buyers at this page as well.

## Create a session and redirect the buyer

### Step 1: Create the Checkout Session

Give each cart line a name, a unit price and a quantity. Prices are integers in the currency's smallest unit, so `2000` is $20.00. Add `success_url`, where the buyer lands after paying, and `cancel_url`, where the buyer goes on giving up. Fluveo writes the session's id in place of `{CHECKOUT_SESSION_ID}` in `success_url`.

**Create a Checkout Session**

cURL:

```bash
curl https://api.fluveo.com/v1/checkout/sessions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: cart-88-session" \
  -d mode=payment \
  -d "line_items[0][price_data][currency]=usd" \
  -d "line_items[0][price_data][unit_amount]=2000" \
  -d "line_items[0][price_data][product_data][name]=Blue T-shirt" \
  -d "line_items[0][quantity]=1" \
  --data-urlencode "success_url=https://shop.example.com/thanks?session={CHECKOUT_SESSION_ID}" \
  --data-urlencode "cancel_url=https://shop.example.com/cart" \
  -d client_reference_id=cart-88
```

Node.js:

```javascript
const response = await fetch("https://api.fluveo.com/v1/checkout/sessions", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "cart-88-session",
  },
  body: new URLSearchParams({
    mode: "payment",
    "line_items[0][price_data][currency]": "usd",
    "line_items[0][price_data][unit_amount]": "2000",
    "line_items[0][price_data][product_data][name]": "Blue T-shirt",
    "line_items[0][quantity]": "1",
    success_url: "https://shop.example.com/thanks?session={CHECKOUT_SESSION_ID}",
    cancel_url: "https://shop.example.com/cart",
    client_reference_id: "cart-88",
  }),
});
const session = await response.json();
console.log(session.url);
```

Python:

```python
import requests

response = requests.post(
    "https://api.fluveo.com/v1/checkout/sessions",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "cart-88-session",
    },
    data={
        "mode": "payment",
        "line_items[0][price_data][currency]": "usd",
        "line_items[0][price_data][unit_amount]": 2000,
        "line_items[0][price_data][product_data][name]": "Blue T-shirt",
        "line_items[0][quantity]": 1,
        "success_url": "https://shop.example.com/thanks?session={CHECKOUT_SESSION_ID}",
        "cancel_url": "https://shop.example.com/cart",
        "client_reference_id": "cart-88",
    },
)
session = response.json()
print(session["url"])
```

PHP:

```php
<?php
$ch = curl_init("https://api.fluveo.com/v1/checkout/sessions");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: cart-88-session",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "mode" => "payment",
        "line_items" => [[
            "price_data" => [
                "currency" => "usd",
                "unit_amount" => 2000,
                "product_data" => ["name" => "Blue T-shirt"],
            ],
            "quantity" => 1,
        ]],
        "success_url" => "https://shop.example.com/thanks?session={CHECKOUT_SESSION_ID}",
        "cancel_url" => "https://shop.example.com/cart",
        "client_reference_id" => "cart-88",
    ]),
]);
$session = json_decode(curl_exec($ch), true);
echo $session["url"];
```

### Step 2: Send the buyer to the url

The session's id starts with `cs_test_`. It is `open` and `unpaid`, and `url` is its page. Redirect the buyer's browser there. `payment_intent` is `null` until the buyer presses pay on the page; from then on it holds the `pi_` id.

*Response (trimmed)*

```json
{
  "id": "cs_test_...",
  "object": "checkout.session",
  "mode": "payment",
  "status": "open",
  "payment_status": "unpaid",
  "amount_total": 2000,
  "currency": "usd",
  "payment_intent": null,
  "url": "https://.../c/cs_test_...",
  "expires_at": 1767312000
}
```

### Step 3: Fulfil from the webhook

Ship the order when your endpoint gets `checkout.session.completed`, with `payment_status` `paid`. Fluveo sends `payment_intent.succeeded` beside it. Treat the visit to `success_url` as a thank-you page only. It proves nothing: the address can be typed by hand, and the buyer may never get back to it.

*checkout.session.completed (trimmed)*

```json
{
  "id": "evt_...",
  "type": "checkout.session.completed",
  "data": {
    "object": {
      "id": "cs_test_...",
      "object": "checkout.session",
      "status": "complete",
      "payment_status": "paid",
      "client_reference_id": "cart-88"
    }
  }
}
```

**Parameters you can send**

- `mode` (string, required): `payment` charges the cart. `setup` saves the buyer's card with a SetupIntent and charges nothing: see [Save a card without charging](https://docs.fluveo.com/payments/checkout.md#setup).
- `line_items` (array, optional): Required in `payment` mode, refused in `setup` mode. The cart, one entry per line: `line_items[0][...]`, `line_items[1][...]`. At most 100 lines, every line in one currency.
  - `price_data` (object, required): The price of one unit.
    - `currency` (string, required): A currency your account accepts, lowercase, such as `usd`.
    - `unit_amount` (integer, required): One unit's price, as an integer in the currency's smallest unit.
    - `product_data` (object, required)
      - `name` (string, required): The line's name on the page.
  - `quantity` (integer, optional): Units of this line. Defaults to 1.
- `success_url` (string, optional): The buyer's next stop after paying. `{CHECKOUT_SESSION_ID}` becomes the session's id.
- `cancel_url` (string, optional): The buyer's next stop on leaving the page without paying.
- `client_reference_id` (string, optional): Your id for this cart or order, returned on the session and its events.
- `customer_email` (string, optional): Fills the email box for the buyer.
- `customer` (string, optional): A `cus_` id of your store. Send this or `customer_email`, not both.
- `expires_at` (integer, optional): Unix time at which the page stops taking payment, 30 minutes from now or later. Without it, the page lasts 24 hours.
- `metadata` (object, optional): Key-value pairs of yours, kept on the session.
- `payment_intent_data` (object, optional): Settings for the session's PaymentIntent, read as `POST /v1/payment_intents` reads them: `capture_method`, `description`, `metadata`, `receipt_email`, `setup_future_usage`, `shipping`, `statement_descriptor` and `statement_descriptor_suffix`. A `setup` session refuses it.
- `setup_intent_data` (object, optional): `description` and `metadata` for the SetupIntent of a `setup` session. A `payment` session refuses it.
- `payment_method_types` (array, optional): What the page lets the buyer pay with. Without it, the page offers the methods your account has on in its payment method configuration. See [Payment methods on the page](https://docs.fluveo.com/payments/checkout.md#payment-methods).
- `payment_method_options` (object, optional): `payment_method_options[pix][expires_after_seconds]` (60 to 86400) and `payment_method_options[boleto][expires_after_days]` (0 to 60). Send each one with its method in `payment_method_types`.

## Payment methods on the page

`payment_method_types` sets what the page shows. Without it, the page shows every method your account accepts and has on in its payment method configuration, less the ones the session cannot take: `payment_intent_data[capture_method]=manual` leaves out `pix` and `boleto`, and `payment_intent_data[setup_future_usage]=on_session` leaves out `pix`. The session call and its fields are the same whatever the method.

| `payment_method_types` | On the page | Options |
| --- | --- | --- |
| `card` | A card form | none |
| `pix` | A Pix QR code and a code to copy | `payment_method_options[pix][expires_after_seconds]` |
| `boleto` | A Boleto with its line and PDF | `payment_method_options[boleto][expires_after_days]` |

Send several methods together and the buyer picks one. Each method charges in the currencies listed in [Payment methods](https://docs.fluveo.com/payments.md#payment-methods), and a method your account does not accept answers a 400.

A total under the currency's minimum charge, such as $0.50 (`50`) in `usd`, answers a 400. So does a parameter the session does not take, and the error names it. Every route is in the [API reference](https://docs.fluveo.com/api/checkout_sessions.md).

## Save a card without charging

A session with `mode=setup` charges nothing. Its page asks for the buyer's card and saves it with a SetupIntent, so you can charge that card later with a PaymentIntent. Send no `line_items`: a setup session refuses them. Send `currency` (or `payment_method_types`), and a `customer` to attach the saved card to.

*Create a setup session*

```bash
curl https://api.fluveo.com/v1/checkout/sessions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: cus-42-save-card" \
  -d mode=setup \
  -d currency=usd \
  -d customer=cus_... \
  --data-urlencode "success_url=https://shop.example.com/card-saved?session={CHECKOUT_SESSION_ID}" \
  --data-urlencode "cancel_url=https://shop.example.com/account"
```

The session names its SetupIntent in `setup_intent` (a `seti_` id) from the start. `payment_intent` and `amount_total` are `null`, and `payment_status` stays `no_payment_required` for the whole life of the session. Without `payment_method_types`, the page offers the methods your account has on in its payment method configuration that a session can save; a Pix is never saved.

| `status` | `payment_status` | Meaning |
| --- | --- | --- |
| `open` | `no_payment_required` | The page waits for the buyer's card. |
| `complete` | `no_payment_required` | The card is saved: the SetupIntent is `succeeded` and its `payment_method` is attached to the Customer. |
| `expired` | `no_payment_required` | Closed with nothing saved. The SetupIntent is `canceled`. |

When the buyer saves the card, your endpoint gets `setup_intent.succeeded` and `checkout.session.completed`, with `payment_status` `no_payment_required`. Read the saved `pm_` from the SetupIntent's `payment_method` with `GET /v1/setup_intents/{id}`.

## Session statuses

| `status` | `payment_status` | Meaning |
| --- | --- | --- |
| `open` | `unpaid` | The page waits for the buyer: nothing paid yet, a card attempt running, or a Pix QR not yet paid. |
| `complete` | `paid` | Paid. The PaymentIntent is `succeeded`. |
| `complete` | `unpaid` | The card is authorized and waits for your capture (`payment_intent_data[capture_method]=manual`), or a Boleto is issued and not yet paid. |
| `expired` | `unpaid` | Closed unpaid, by `expires_at` or by you. Its PaymentIntent is canceled. |

## Other calls

- `GET /v1/checkout/sessions/{id}` returns one session. Your `success_url` page can call it to pick its message.
- `GET /v1/checkout/sessions` returns sessions, newest first. Narrow it with `status` (`open`, `complete`, `expired`), `payment_intent`, `customer` or `created`.
- `GET /v1/checkout/sessions/{id}/line_items` returns the cart lines.
- `POST /v1/checkout/sessions/{id}` updates `metadata` and nothing else.
- `POST /v1/checkout/sessions/{id}/expire` closes an open session now, say after the cart changed. Its PaymentIntent is canceled and `checkout.session.expired` goes out. A paid session refuses it.

*Expire a session*

```bash
curl -X POST https://api.fluveo.com/v1/checkout/sessions/cs_test_.../expire \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: cart-88-expire"
```

## Events

| Event | When |
| --- | --- |
| `checkout.session.completed` | The buyer paid, or a `setup` session saved the card. With manual capture, the card was authorized; with a Boleto, it was issued: `payment_status` is then `unpaid`. A Pix sends it once paid. |
| `checkout.session.async_payment_succeeded` | An issued Boleto was paid. |
| `checkout.session.async_payment_failed` | An issued Boleto expired unpaid. |
| `checkout.session.expired` | The session closed unpaid, at `expires_at` or by your call. |

The session's PaymentIntent sends its own events too. They are listed on [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md#events).

## Test it

1. Create a session with a `sk_test_` key, as above, and open its `url` in a browser.
2. Pay with `4000 0000 0000 0002`, any future expiry and any CVC. The page shows the decline and stays open.
3. Pay with `4242 4242 4242 4242`. The browser goes to your `success_url`.
4. `GET` the session. `status` is `complete`, `payment_status` is `paid`, and your endpoint got `checkout.session.completed`.
5. Create another session and expire it. `status` is `expired`, and `checkout.session.expired` arrives.
6. Repeat the create with the same `Idempotency-Key` and body. The same `cs_test_` comes back, with `Idempotent-Replayed: true`.

> **For AI agents:**
>
> Hosted checkout for agents: POST /v1/checkout/sessions with mode=payment, line_items[0][price_data][currency] (a currency the account accepts), line_items[0][price_data][unit_amount] (smallest unit), line_items[0][price_data][product_data][name], line_items[0][quantity], success_url, cancel_url and an Idempotency-Key. Redirect the buyer to the session's url. Fulfil only on checkout.session.completed (signed) or a GET that shows payment_status=paid. payment_method_types is any of card, pix and boleto, with their payment_method_options; without it the page offers every method the account accepts and has on in its payment method configuration, less pix and boleto beside payment_intent_data[capture_method]=manual and less pix beside payment_intent_data[setup_future_usage]=on_session. mode=setup saves a card with a SetupIntent and charges nothing: no line_items, send currency and customer; amount_total is null, payment_status is no_payment_required, and setup_intent.succeeded with checkout.session.completed say the card is saved. Each method charges in the currencies of the /payments methods table.

## Next

- [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md) with the API alone.
- [Refund a payment](https://docs.fluveo.com/payments/refunds.md)
- [Webhooks](https://docs.fluveo.com/webhooks.md)
