<!--
  SINGLE-SOURCE SYNC (ADR-0021): canonical prose for /docs/checkout-sessions, mirrored 1:1 by
  resources/js/pages/public/docs/checkout-sessions.tsx — edit BOTH in the same change (engineering-rules P4).
  Sourced from docs/01-architecture/10-api-design.md §6.1, §6.2 and docs/01-architecture/13-brokeret-crm-connector.md §3.
-->

# Checkout Sessions

A Checkout Session is the standard integration path: you create one server-side, then send the customer
to the hosted Cashier at the returned `url`. The Cashier shows the payment methods your Routing Rules
and Provider capabilities allow, drives the Payment, and the result reaches you by signed webhook.

## Create a session

```
POST /api/v1/checkout_sessions
Authorization: Bearer sk_test_...
Idempotency-Key: your-deposit-id
Content-Type: application/json

{
  "amount": "500.00",
  "asset_code": "USDT.TRC20",
  "customer": { "external_ref": "12345", "email": "trader@example.com", "country": "MY" },
  "success_url": "https://your-app.example/deposits/success",
  "cancel_url": "https://your-app.example/deposits/cancel",
  "metadata": { "account_ref": "MT5-882130", "crm_request_id": "your-deposit-id" }
}
```

```json
201 Created

{
  "id": "cs_01J9ZW3XQ4R8",
  "object": "checkout_session",
  "status": "open",
  "amount": "500.00",
  "asset_code": "USDT.TRC20",
  "country": "MY",
  "customer": "cust_01J9ZR2KD7VE",
  "url": "https://payxiro.com/c/cs_01J9ZW3XQ4R8",
  "payment": null,
  "expires_at": "2026-07-05T10:41:22Z",
  "metadata": { "account_ref": "MT5-882130", "crm_request_id": "your-deposit-id" },
  "created_at": "2026-07-05T09:41:22Z"
}
```

Redirect the customer to `url`. Once they commit, a Payment (`pay_...`) is created and linked on the
session (`payment`), and the outcome arrives by webhook.

## Request fields

The body's top-level `additionalProperties` is `false` — send only these keys:

| Field                        | Notes                                                                                                                                   |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `amount`                     | Decimal string (`"500.00"`). Omit for an open-amount session (see below).                                                               |
| `asset_code`                 | Required. Registry code, e.g. `USDT.TRC20` or `MYR`.                                                                                    |
| `customer`                   | `external_ref` (required), plus optional `email`, `phone`, `country`, `kyc_status`, `custom_fields`. PayXiro upserts on `external_ref`. |
| `success_url` / `cancel_url` | Must match the Platform's allowed redirect patterns (configured in the Portal).                                                         |
| `metadata`                   | Free-form key/values echoed back on the Payment and in webhooks — put your own lookup keys here.                                        |
| `context`                    | Informational risk context: `kyc_status`, `customer_ip`, `customer_since`.                                                              |

## Redirect-URL rules

`success_url` and `cancel_url` **must** fall within the Platform's allowed redirect patterns (set in
the Merchant Portal). A URL outside them is rejected, with `param` naming the offending field. These
redirects are a UX signal only — the trusted signal for crediting is always the webhook.

`amount` must fit the asset's display decimals: `"500.001"` is rejected for a 2-decimal asset.

## Open-amount deposits

Omit `amount` and send `"open_amount": true` instead to create a session with no fixed amount — the
Cashier shows "send any amount" and the Payment settles at whatever arrives. `asset_code` stays
required; `amount` and `open_amount` are mutually exclusive (sending both is `invalid_request`). The
resulting Payment returns `"amount": null` until (and after) settlement, with the settled figure in
`received_amount`. Open-amount routes only to Provider Accounts that declare the `open_amount`
capability.

```
POST /api/v1/checkout_sessions
Idempotency-Key: your-deposit-id

{
  "open_amount": true,
  "asset_code": "USDT.TRC20",
  "customer": { "external_ref": "12345", "country": "MY" },
  "success_url": "https://your-app.example/deposits/success",
  "cancel_url": "https://your-app.example/deposits/cancel"
}
```

## Retrieve a session

```
GET /api/v1/checkout_sessions/{id}
Authorization: Bearer sk_test_...
```

Returns the session with its current `status` (`open` → `completed` | `expired` | `cancelled`) and the
linked `payment` once one exists. Next: choose an [integration mode](/docs/integration-modes).
