> ## 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.

# Invoices

> List invoices, read one in detail, preview renewals, and collect payment.

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

Hooks for the invoice surfaces of a customer portal — the history list, a single invoice detail page, the next-renewal preview, and paying an outstanding balance. Amounts are always in **minor units** (e.g. cents).

<LearnHooksTip />

Every hook returns loading/error state — reads via `isLoading: boolean`, the mutation via `isPending: boolean` — plus `error: Error | null`. See [Introduction](/react/introduction). The sections below list each hook's specific returns.

## useInvoices

Fetches the full invoice history for the customer in context as one cached list, plus a `downloadPdf(id)` helper that opens a row's finalized PDF in a new tab. Takes no arguments; the customer comes from `<UnitPayProvider>` context.

**Returns**

* `invoices` — `Invoice[]` — every invoice on the customer, or `[]` while loading or when none exist. `status` mirrors the DB enum (`draft` · `issued` · `partially_paid` · `paid` · `overdue` · `void` · `uncollectible`); amounts (`totalAmount`, `amountDue`, `amountPaid`) are in minor units.
* `downloadPdf(invoiceId)` — opens the matching invoice's `pdfUrl` in a new tab. No-op when the row has no link yet (still finalizing) or when called outside the browser — grey out the button on `!invoice.pdfUrl`.

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

export default function InvoiceList() {
  const { invoices, downloadPdf, isLoading, error } = useInvoices();

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Failed to load invoices.</p>;
  if (invoices.length === 0) return <p>No invoices yet.</p>;

  return (
    <ul>
      {invoices.map((inv) => (
        <li key={inv.id}>
          {inv.invoiceNumber ?? 'Draft'} — {inv.status}
          <button onClick={() => downloadPdf(inv.id)} disabled={!inv.pdfUrl}>PDF</button>
        </li>
      ))}
    </ul>
  );
}
```

<Expandable title="Invoice object">
  ```json theme={null}
  {
    "id": "inv_01k2x7m3q0abcdef",
    "customerId": "cus_01ktxbxs2re8nsdfmzxw0gh988",
    "subscriptionId": "sub_01k2x6...",
    "status": "issued",
    "invoiceNumber": "INV-1042",
    "currency": "usd",
    "totalAmount": 4900,
    "amountDue": 4900,
    "amountPaid": 0,
    "collectionMethod": "charge_automatically",
    "netPaymentTermDays": null,
    "dueDate": "2026-07-01T00:00:00.000Z",
    "paidAt": null,
    "createdAt": "2026-06-01T00:00:00.000Z",
    "pdfUrl": "https://files.useunitpay.com/invoices/INV-1042.pdf"
  }
  ```
</Expandable>

## useInvoice

Loads the full payload for a single invoice — the header fields plus its line `items` and successful `payments` — for an invoice detail page. The server filters payments to `status = 'succeeded'`, so the portal never has to hide retry noise.

**Parameters**

<ParamField body="invoiceId" type="string" required>
  The invoice to fetch. The query is disabled (and `invoice` stays `null`) while this is `null` / `undefined`.
</ParamField>

**Returns**

* `invoice` — `InvoiceDetail | null` — the invoice plus `items: InvoiceItem[]` and `payments: InvoicePayment[]`, or `null` while loading. Amounts are in minor units.
* `downloadPdf()` — opens the invoice's `pdfUrl` in a new tab. No-op until the PDF finalizes server-side or when called outside the browser — grey out the button on `!invoice.pdfUrl`.

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

export default function InvoiceDetail({ invoiceId }: { invoiceId: string }) {
  const { invoice, downloadPdf, isLoading, error } = useInvoice(invoiceId);

  if (isLoading) return <p>Loading…</p>;
  if (error || !invoice) return <p>Couldn’t load this invoice.</p>;

  return (
    <article>
      <h1>{invoice.invoiceNumber ?? 'Draft'}</h1>
      <p>Status: {invoice.status}</p>
      <ul>
        {invoice.items.map((item) => (
          <li key={item.id}>{item.description} — {item.quantity} × {item.unitAmount}</li>
        ))}
      </ul>
      <button onClick={downloadPdf} disabled={!invoice.pdfUrl}>Download PDF</button>
    </article>
  );
}
```

<Expandable title="InvoiceDetail object">
  ```json theme={null}
  {
    "id": "inv_01k2x7m3q0abcdef",
    "customerId": "cus_01ktxbxs2re8nsdfmzxw0gh988",
    "subscriptionId": "sub_01k2x6...",
    "status": "partially_paid",
    "invoiceNumber": "INV-1042",
    "currency": "usd",
    "totalAmount": 4900,
    "amountDue": 1900,
    "amountPaid": 3000,
    "collectionMethod": "charge_automatically",
    "netPaymentTermDays": null,
    "dueDate": "2026-07-01T00:00:00.000Z",
    "paidAt": null,
    "createdAt": "2026-06-01T00:00:00.000Z",
    "pdfUrl": "https://files.useunitpay.com/invoices/INV-1042.pdf",
    "items": [
      {
        "id": "ili_01k2x7...",
        "invoiceId": "inv_01k2x7m3q0abcdef",
        "description": "Pro plan — June",
        "quantity": 1,
        "unitAmount": 4900,
        "amount": 4900,
        "currency": "usd"
      }
    ],
    "payments": [
      {
        "id": "pay_01k2x8...",
        "amount": 3000,
        "currency": "usd",
        "source": "online",
        "paymentMethodType": "card",
        "externalReference": null,
        "paidAt": "2026-06-05T12:00:00.000Z"
      }
    ]
  }
  ```
</Expandable>

## useUpcomingInvoice

Previews the next renewal for a subscription — the "you'll be charged \$X on date Y" portal card. It's a pure read (`GET /v1/subscriptions/:id/upcoming-invoice`) that never mutates state; the server computes the line items from the current sub config (plan + addons + usage so far) at request time.

**Parameters**

<ParamField body="subscriptionId" type="string">
  The subscription to preview. The query is disabled (and `invoice` stays `null`) while this is omitted.
</ParamField>

**Returns** — `invoice: UpcomingInvoice | null` — the renewal preview, or `null` when there is no upcoming renewal (canceled / ended subs, or before trial end) and while loading. Amounts are in minor units. `creditApplied` is always `0` in the preview.

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

export default function NextCharge({ subscriptionId }: { subscriptionId: string }) {
  const { invoice, isLoading, error } = useUpcomingInvoice(subscriptionId);

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Couldn’t load your next invoice.</p>;
  if (!invoice) return <p>No upcoming renewal.</p>;

  return (
    <p>
      Next charge: {invoice.amountDue} {invoice.currency} on{' '}
      {new Date(invoice.billingDate).toLocaleDateString()}
    </p>
  );
}
```

<Expandable title="UpcomingInvoice object">
  ```json theme={null}
  {
    "subscriptionId": "sub_01k2x6...",
    "planId": "plan_01k2x5...",
    "planName": "Pro",
    "currency": "usd",
    "billingPeriodStart": "2026-07-01T00:00:00.000Z",
    "billingPeriodEnd": "2026-07-31T23:59:59.999Z",
    "billingDate": "2026-07-01T00:00:00.000Z",
    "items": [
      { "description": "Pro plan", "quantity": 1, "unitPrice": 4900, "amount": 4900, "kind": "plan_fee" },
      { "description": "API calls", "quantity": 12000, "unitPrice": 1, "amount": 12000, "kind": "metered_usage" }
    ],
    "subtotal": 16900,
    "discountAmount": 0,
    "taxAmount": 0,
    "totalAmount": 16900,
    "amountDue": 16900,
    "minimumSpend": 0,
    "creditApplied": 0
  }
  ```
</Expandable>

## usePayInvoice

Mutation that pays one outstanding invoice. The server picks the path — inline charge of a saved card, a hosted recovery form, or no action — and resolves to a [`SettleOutcome`](/react/settlement). Pass `on*` settlement handlers to wire your UX to each outcome.

**Parameters**

<ParamField body="invoiceId" type="string" required>
  The invoice to collect payment for.
</ParamField>

<ParamField body="options" type="HandleSettlementOptions">
  Settlement callbacks dispatched after the mutation resolves — `onChargedInline`, `onRequiresForm`, `onNoAction`, etc. See the [Settlement model](/react/settlement).
</ParamField>

The `pay(input?)` alias accepts an optional input:

<ParamField body="input.idempotencyKey" type="string">
  Override the auto-generated `Idempotency-Key`.
</ParamField>

<ParamField body="input.forceForm" type="boolean">
  Force the server to mint `requires_form` even when a payment method is on file — the customer always sees the inline `<PaymentForm>` and explicitly confirms, instead of a silent off-session debit. Portal callers typically set this `true`.
</ParamField>

**Returns** — a TanStack `UseMutationResult` extended with `pay(input?) => Promise<SettleOutcome>`. On success the hook invalidates the single-invoice cache and the full customer scope (settling an invoice can move money, change credit balance, and unblock a `past_due` sub), then dispatches your `options` handlers. The server picks the path:

* Customer has a PM **and** `amountDue > 0` **and** no `forceForm` → inline charge → `charged_inline`
* No PM, or `forceForm`, or the inline charge fails → mints a recovery checkout → `requires_form`
* Already paid, or `amountDue === 0` → `no_action`

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

export default function PayInvoiceButton({ invoiceId }: { invoiceId: string }) {
  const [form, setForm] = useState<null | { token: string }>(null);

  const { pay, isPending } = usePayInvoice(invoiceId, {
    onChargedInline: () => alert('Paid!'),
    onRequiresForm: (result) => setForm({ token: result.client.token }),
    onNoAction: () => alert('Nothing to pay.'),
  });

  if (form) {
    // Mount <PaymentForm clientSecret={form.token} … /> (Stripe Elements).
    return <PaymentForm clientSecret={form.token} />;
  }

  return (
    <button onClick={() => pay({ forceForm: true })} disabled={isPending}>
      {isPending ? 'Processing…' : 'Pay now'}
    </button>
  );
}
```

<Note>
  `forceForm` is a per-call input — pass it to `pay({ forceForm: true })`, not to the hook's options argument. The second argument (`UsePayInvoiceOptions`, which equals `HandleSettlementOptions`) carries only your settlement callbacks. The `pay()` input is where `idempotencyKey` and `forceForm` overrides live.
</Note>

## See also

<CardGroup cols={2}>
  <Card title="Settlement model" icon="money-bill-transfer" href="/react/settlement">
    The full `SettleOutcome` union and `handleSettlement` dispatch behind `usePayInvoice`.
  </Card>

  <Card title="Payment methods" icon="credit-card" href="/react/payment-methods">
    Capture a card before paying, or replace an in-use default.
  </Card>

  <Card title="Subscriptions" icon="repeat" href="/react/subscriptions">
    The subscriptions these invoices bill for.
  </Card>

  <Card title="Customer" icon="user" href="/react/customer">
    `payInvoice()` on `useCustomer` mints a hosted-link alternative.
  </Card>
</CardGroup>
