<!--
  SINGLE-SOURCE SYNC (ADR-0021): canonical prose for /docs/payments, mirrored 1:1 by
  resources/js/pages/public/docs/payments.tsx — edit BOTH in the same change (engineering-rules P4).
  Sourced from docs/01-architecture/10-api-design.md §6.2, §6, docs/01-architecture/07-payment-flows.md §2.1,
  and docs/01-architecture/13-brokeret-crm-connector.md §6. State machine is canonical — changing it needs an ADR.
-->

# Payments

Most integrations use [Checkout Sessions](/docs/checkout-sessions) and never touch the Payments API
directly. Use it when your platform renders its own UI (mobile, in-terminal, back-office) and already
knows the method — the [headless mode](/docs/integration-modes).

## Create a Payment directly

```
POST /api/v1/payments
Authorization: Bearer sk_test_...
Idempotency-Key: your-deposit-id

{
  "amount": "500.00",
  "asset_code": "USDT.TRC20",
  "payment_method": "crypto",
  "country": "AE",
  "customer": "cust_01J9ZR2KD7VE",
  "metadata": { "crm_transaction_id": "DEP-448901" }
}
```

```json
201 Created

{
  "id": "pay_01J9ZX8KT2M1",
  "object": "payment",
  "status": "requires_action",
  "amount": "500.00",
  "asset_code": "USDT.TRC20",
  "payment_method": "crypto",
  "country": "AE",
  "customer": "cust_01J9ZR2KD7VE",
  "provider_account": "pacc_01J8XN5QW9BC",
  "next_action": {
    "type": "crypto_deposit",
    "deposit_address": "TWd4gh8N1tK9qkGf2vX7...",
    "network": "TRC20",
    "expires_at": "2026-07-05T11:41:22Z"
  },
  "metadata": { "crm_transaction_id": "DEP-448901" },
  "created_at": "2026-07-05T09:41:22Z"
}
```

Open-amount works the same as sessions: omit `amount`, send `"open_amount": true`. `asset_code` stays
required and the two are mutually exclusive.

## Card payments (Stripe)

Card is available via Stripe: send `"payment_method": "card"` with a fiat `asset_code` (USD and EUR to
start). Card details are entered only on Stripe's own surfaces — PayXiro never sees the card number.
Each Stripe provider account picks one of two presentations:

- **Redirect** (default) — `next_action.type` is `redirect`; send the customer to `redirect_url`
  (Stripe's hosted Checkout page).
- **Embedded** — `next_action.type` is `provider_component`, carrying
  `{ "provider": "stripe", "client_secret", "publishable_key" }`. The hosted Cashier mounts Stripe's
  embedded Checkout in place; a headless client loads Stripe.js and calls
  `stripe.initEmbeddedCheckout({ clientSecret })`.

As always, credit only on the signed webhook — never on the browser return.

## The Payment state machine

These are the only Payment states. `requires_action` waits on the customer or a provider action;
`under_review` waits on a risk decision and resolves to `processing` or `failed`.

| Status            | Meaning                                                                                |
| ----------------- | -------------------------------------------------------------------------------------- |
| `created`         | Payment recorded, not yet routed to a provider action                                  |
| `requires_action` | Waiting on the customer or a provider redirect/action (e.g. send crypto to an address) |
| `under_review`    | Held for a risk decision (rule outcome or the manual review queue)                     |
| `processing`      | Provider is settling; awaiting confirmation                                            |
| `succeeded`       | Terminal — funds confirmed; the credit-worthy signal                                   |
| `failed`          | Terminal — declined, `routing_exhausted`, or a risk `block`                            |
| `cancelled`       | Terminal — withdrawn before `processing`                                               |
| `expired`         | Terminal — the action window elapsed with no completion                                |

Legal transitions: `created → requires_action | under_review | processing | failed | cancelled |
expired`; `requires_action → under_review | processing | cancelled | expired`; `under_review →
processing | failed`; `processing → succeeded | failed`. Every transition is recorded on the
transaction timeline and emitted as a `payment.*` event.

## Manual methods: submit-form

For manual methods (e.g. manual bank), post the dynamic-form values the method defines:

```
POST /api/v1/payments/{id}/submit-form
Authorization: Bearer sk_test_...
```

The values are validated against the method's `form_definition`, stored as `form_values`, and the
Payment transitions `requires_action → under_review`. This call is not money-creating (no
`Idempotency-Key`) and is idempotent on state. Fetch the method's form definition first — fields are
data, not code.

## Poll status (reconciliation fallback)

If a webhook is delayed or missed, reconcile by fetching the Payment:

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

Returns `404` (never `403`) if the Payment is unknown or belongs to another tenant. Poll as a
scheduled sweep over Payments still pending after N minutes — not as the primary channel. When a poll
shows `succeeded`, run the **same idempotent credit path** the webhook uses (keyed on `payment.id`),
so a poll and a late webhook cannot double-credit.
