> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useunitpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout

> Preview what the customer owes before they commit, then poll a hosted-form session to completion.

import LearnHooksTip from '/snippets/learn-hooks-tip.mdx';

Two hooks bracket a checkout: `useCheckoutPreview` shows what the customer owes today and what changes before anything is charged, and `useCheckoutSession` polls a hosted-form session until the PSP confirms. Both are scoped to the portal customer.

<LearnHooksTip />

Every hook returns `isLoading: boolean` and `error: Error | null` (see [Introduction](/react/introduction)); the sections below list each hook's specific returns.

## useCheckoutPreview

Returns a server-computed preview of a checkout action — amount due today, the upcoming schedule, credit impact, line items, and warnings — before anything is charged. It calls `POST /v1/checkout/sessions/preview` with a discriminated input. Pass `null` to disable (e.g. while inputs are still being assembled). `customerId` is filled from context — callers omit it.

<Note>
  For plan-**change** previews, use [`useSubscription(id).previewChange()`](/react/subscriptions) instead — it returns a richer shape (proration, entitlement diffs, direction/timing).
</Note>

**Parameters**

<ParamField body="input" type="CheckoutPreviewInput | null" required>
  The action to preview, a discriminated union on `purpose`. Pass `null` to hold the query inert.

  <Expandable title="purpose variants">
    <ParamField body="subscription" type="{ purpose: 'subscription'; planId: string; subscriptionId?: string; amountCents?: number }">
      Preview subscribing to (or starting) a plan.
    </ParamField>

    <ParamField body="addon_purchase" type="{ purpose: 'addon_purchase'; planId: string; subscriptionId: string }">
      Preview attaching an addon plan to an existing subscription.
    </ParamField>

    <ParamField body="credit_package" type="{ purpose: 'credit_package'; creditPackageId: string }">
      Preview buying a fixed credit package.
    </ParamField>

    <ParamField body="credit_topup" type="{ purpose: 'credit_topup'; creditCurrencyId: string; amountCents: number }">
      Preview a custom-amount credit top-up into one wallet.
    </ParamField>

    <ParamField body="invoice_payment" type="{ purpose: 'invoice_payment'; invoiceId: string }">
      Preview paying an outstanding invoice.
    </ParamField>

    <ParamField body="payment_method_update" type="{ purpose: 'payment_method_update' }">
      Preview a payment-method update (no charge).
    </ParamField>
  </Expandable>
</ParamField>

**Returns**

* `preview` — the `CheckoutPreviewResult`, or `null` while loading / when disabled. It extends the shared base shape with the per-purpose `details` payload:

<ParamField body="dueToday" type="DueToday">Discriminated by `kind`: `charge_now` (`cents`, `currency`), `invoice_later` (`cents`, `netDays`, `estimatedDate`), or `no_charge`.</ParamField>
<ParamField body="lineItems" type="PreviewLineItem[]">`{ description, unitAmount, quantity, subtotal, muted? }` rows.</ParamField>
<ParamField body="subtotal / discount / creditsApplied / tax / total" type="number">Money breakdown in minor units.</ParamField>
<ParamField body="schedule" type="PreviewSchedule">Optional `{ date, amountCents? }` for the next charge.</ParamField>
<ParamField body="creditImpact" type="CreditImpact">Optional wallet delta — `{ currencyName, denomination, minorUnitScale, currentBalance, delta, afterBalance, … }`.</ParamField>
<ParamField body="warnings" type="PreviewWarning[]">`{ code, params? }` — e.g. `trial_requires_payment_method`, `customer_has_no_payment_method`.</ParamField>
<ParamField body="purpose / mode / details" type="-">Echoes the purpose; `details` is the per-purpose payload (e.g. `details.subscription`, `details.creditTopup`).</ParamField>

* `refresh()` — `() => Promise<void>` — refetch the preview (e.g. after the customer edits the amount).

```tsx theme={null}
import { useCheckoutPreview } from '@unitpay/react';

export default function TopUpPreview({ creditCurrencyId }: { creditCurrencyId: string }) {
  const { preview, isLoading, error, refresh } = useCheckoutPreview({
    purpose: 'credit_topup',
    creditCurrencyId,
    amountCents: 2000,
  });

  if (isLoading) return <div>Calculating…</div>;
  if (error || !preview) return <div>Couldn’t preview.</div>;

  return (
    <div>
      {preview.dueToday.kind === 'charge_now' && (
        <div>Due today: {preview.dueToday.cents / 100} {preview.dueToday.currency}</div>
      )}
      {preview.creditImpact && (
        <div>New balance: {preview.creditImpact.afterBalance}</div>
      )}
      {preview.warnings.map((w) => <div key={w.code}>⚠ {w.code}</div>)}
      <button onClick={() => refresh()}>Recalculate</button>
    </div>
  );
}
```

## useCheckoutSession

Polls `GET /v1/checkout/sessions/:id/status` and reports when a hosted-form payment resolves. Use it to await the result of a `requires_form` [SettleOutcome](/react/settlement) — after the Elements form submits, the webhook completer flips the session to `completed` once the PSP confirms. Polling is tiered: \~1s for the first 30s (hot phase), then \~5s, then it stops and synthesizes `expired` at `maxWaitMs`.

**Parameters**

<ParamField body="checkoutSessionId" type="string | null" required>
  The session to poll. Pass `null` to leave the hook idle (e.g. before the form step is reached). Changing the id resets the timeout.
</ParamField>

<ParamField body="options.enabled" type="boolean" default="true">
  Gate polling without unmounting the hook. Set `false` to hold the query inert (e.g. before the modal advances to its confirming step).
</ParamField>

<ParamField body="options.maxWaitMs" type="number" default="90000">
  Wall-clock cap on the pending window. After this elapses the hook stops polling and surfaces `status: 'expired'` even if the server row is still `pending`.
</ParamField>

**Returns**

* `status` — `'idle' | 'pending' | 'completed' | 'expired'`:
  * `'idle'` — no `checkoutSessionId`, or `enabled` is `false`.
  * `'pending'` — polling; defaults to this until the first fetch resolves.
  * `'completed'` — the PSP confirmed and the session row finalized.
  * `'expired'` — terminal server status, or the client-side `maxWaitMs` timeout fired.
* `data` — the `CheckoutSessionStatus` row, or `null` before the first response: `{ id, status, completedAt, subscriptionId, invoiceId }`.

```tsx theme={null}
import { useCheckoutSession } from '@unitpay/react';
import { useEffect } from 'react';

export default function ConfirmingStep({
  sessionId,
  onDone,
}: {
  sessionId: string;
  onDone: (subscriptionId: string | null) => void;
}) {
  const { status, data } = useCheckoutSession(sessionId, { maxWaitMs: 30_000 });

  useEffect(() => {
    if (status === 'completed') onDone(data?.subscriptionId ?? null);
  }, [status, data, onDone]);

  if (status === 'completed') return <div>All set ✓</div>;
  if (status === 'expired') return <div>Payment successful — provisioning is taking longer than usual.</div>;
  return <div>Confirming your payment…</div>;
}
```

<Tip>
  Pair this with a `requires_form` outcome: render the [`<PaymentForm>`](/react/entitlements-and-gates) Elements step, then mount `useCheckoutSession(session.id)` to await provisioning.
</Tip>

## See also

<CardGroup cols={2}>
  <Card title="Catalog & pricing" icon="tags" href="/react/catalog-and-pricing">
    The plans and packages a checkout starts from.
  </Card>

  <Card title="Settlement model" icon="arrows-split-up-and-left" href="/react/settlement">
    Every `SettleOutcome` kind and its callback, including `requires_form`.
  </Card>

  <Card title="Credits & wallets" icon="coins" href="/react/credits-and-wallets">
    Top-up and package purchases that feed the preview.
  </Card>
</CardGroup>
