# Flow A — Checkout

Flow A is **merchant checkout**: your server creates a fixed-amount session with a **server-pinned destination**, and the browser renders a drop-in widget that collects the payment. The browser only ever sees a publishable key (`pk_`) and a session-scoped `client_secret` — never your secret key (`sk_`) — and it cannot change the amount or the destination.

> **Drop-in first, headless below.** Prefer your own UI? The same engine is exposed as the `useDepositSession` hook — see the [Headless](/docs/flow-a-checkout.md) section at the bottom of this page.

## End-to-end

1. **Server creates the session.** Your backend calls `POST /v1/payment_sessions` with `sk_` and an `amount` (expected *net* USDC, after both fees, in base units). The amount is fixed at creation and never updated. Response: `{ session_id, client_secret, expires_at }` (30-min TTL). SDK helper: `createPaymentSession(apiUrl, sk, { amount, order_id })` from `@peltier/node`.
2. **Hand `client_secret` + `pk_` to the browser.** Pass them to `<PeltierCheckout>` along with the same destination you pinned via `PUT /v1/destinations`.
3. **Widget runs the deposit flow.** The buyer sends any token on any supported chain; on detection the widget commits a burner-signed Intent. Every browser read / commit / SSE call is gated by `pk_` + matching `client_secret`.
4. **Destination verified at commit.** The server rejects the commit if the Intent's `destAddr` / `destChainId` don't match the merchant's pin — a buyer who tampers with it only gets a rejected commit, and the contract re-enforces the recipient on-chain. (These props are **required**; the server verifies them, it does not resolve them for you.)
5. **Reconcile.** Listen for [webhooks](/docs/webhooks.md) or poll `GET /v1/payment_sessions/:id` (`sk_`) for the final `status` + `payment_outcome`.

## Drop-in (`<PeltierCheckout>`)

`<PeltierCheckout>` is the entire payer side: hand it the two **per-session** credentials (`sessionId` + `clientSecret`) plus your pinned destination and it runs the burner → detect → commit → settle loop. The publishable key is **inherited from `<PeltierProvider>`** — pass `publishableKey` on the widget only to override it. (If no key resolves — none on the provider and none passed — the widget silently falls back to the anonymous flow and never attaches to your session; the provider makes `publishableKey` required, so this can't happen by accident.)

```typescript
import { PeltierProvider, PeltierCheckout } from '@peltier/react'
import '@peltier/react/styles.css'

// Your SERVER created the session (createPaymentSession, sk_) and passed
// { sessionId, clientSecret } down to the browser.
<PeltierProvider apiUrl="https://api.peltier.dev" publishableKey="pk_live_…">
  <PeltierCheckout
    sessionId="a1b2c3d4-…"
    clientSecret="cs_9f1c…e2"
    destinationAddress="0xYourPinnedDestination…"  // MUST equal PUT /v1/destinations — verified at commit
    destinationChainId={8453}
    onComplete={(sessionId) => console.log('paid', sessionId)}  // the SESSION id, not a tx hash
  />
</PeltierProvider>
```

> `<PeltierCheckout>` is a thin wrapper over `<DepositWidget>` that makes the two per-session credentials (`sessionId` + `clientSecret`) **required by its type**; the publishable key comes from the provider. `<DepositWidget>` accepts the same props.

**React Native.** The same flow ships as `<DepositWidget>` in `@peltier/react-native` — pass `sessionId` + `clientSecret` (the publishable key is inherited from `<PeltierProvider>`). See [React Native](/docs/react-native.md).

## Session endpoints

**POST** `/v1/payment_sessions` `sk_`

```json
// POST body
{
  "amount": "10000000",        // expected NET USDC (uint string, base units)
  "currency": "USDC",          // optional merchant reference
  "order_id": "ord_42",        // optional
  "metadata": { "cart": "x" }  // optional JSON
}

// 200 OK
{
  "session_id": "a1b2c3d4-…",
  "client_secret": "cs_9f1c…e2",
  "expires_at": 1714001800
}
```

**GET** `/v1/payment_sessions/:id` `sk_` — returns the final `status` + `payment_outcome` for reconciliation (SDK wrapper `getPaymentSession(apiUrl, sk, sessionId)`). Returns `404` if the session isn't owned by the authenticated merchant (even if it exists for another merchant — IDOR mitigation).

```json
{
  "session_id": "a1b2c3d4-…",
  "status": "swept",
  "amount": "10000000",
  "payment_outcome": "overpaid",
  "detected_amount": "10500000",
  "settled_amount": "10500000",
  "overpaid_amount": "500000",
  "created_at": "2026-06-07T12:00:00Z"
}
```

The full server + webhook walkthrough is in the [5-minute quickstart](/docs/merchant-quickstart.md); over/underpayment semantics are in [amount classification](/docs/amount-classification.md).

## Headless

`useDepositSession` is the same deposit engine `<PeltierCheckout>` / `<DepositWidget>` run, without the DOM — it creates the burner, registers it to your session, watches status over SSE, and runs the detect-then-sign commit. Render `burnerAddress` + `status` however you like.

Your server still mints the session first (`createPaymentSession`, `sk_`) — see the endpoints above and the [5-minute quickstart](/docs/merchant-quickstart.md).

```typescript
import { useDepositSession } from '@peltier/react'

const { burnerAddress, session, status, error, connectionState, retryCommit, reset } = useDepositSession({
  apiUrl: 'https://api.peltier.dev',
  implementationAddress: '0xCc33d64D42E8b5f07A0202bd3a798ef9e951d3EC', // or DEFAULT_IMPLEMENTATION_ADDRESS
  sessionId,          // from createPaymentSession (your server)
  clientSecret,       // from createPaymentSession (your server)
  publishableKey: 'pk_live_…',
  destinationAddress: '0xYourPinnedDestination…',   // the PUT /v1/destinations/{chain_id} pin
  destinationChainId: 8453,
  onStatusChange: (s) => console.log('status', s),   // pending → detected → sweeping → bridging → swept
  onComplete: (id) => console.log('paid', id),       // fires once per session
})

// burnerAddress → the deposit address to QR-encode; status → the live stage.
```

- `error` is a structured `PeltierError` (`{ code, message, retryable, cause }`) or `null` on the happy path.
- `connectionState` reflects the watch transport — `'connecting' | 'live' | 'reconnecting' | 'polling'` — so you can surface a degraded connection instead of a mystery stall.
- `retryCommit()` re-runs a failed detect-then-sign commit — the safe recovery for a funded burner (never `reset()` away a detected deposit; `reset` refuses while a deposit is settling unless forced).

All three checkout credentials (`sessionId` + `clientSecret` + `publishableKey`) must be present **together** — drop any one and the hook falls back to the anonymous (non-merchant) flow and never attaches to your session.

**Reconcile server-side.** Poll `GET /v1/payment_sessions/:id` with your `sk_` (SDK wrapper `getPaymentSession(apiUrl, sk, sessionId)`) or verify the [webhooks](/docs/webhooks.md) — both carry the final `status` + `payment_outcome`.

**React Native.** `useDepositSession` ships identically in `@peltier/react-native`; pass `eventSource` for true SSE (see [React Native](/docs/react-native.md)).
