Skip to main content
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). Every hook returns loading/error state — reads via isLoading: boolean, the mutation via isPending: boolean — plus error: Error | null. See 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
  • invoicesInvoice[] — 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.
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>
  );
}

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
invoiceId
string
required
The invoice to fetch. The query is disabled (and invoice stays null) while this is null / undefined.
Returns
  • invoiceInvoiceDetail | 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.
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>
  );
}

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
subscriptionId
string
The subscription to preview. The query is disabled (and invoice stays null) while this is omitted.
Returnsinvoice: 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.
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>
  );
}

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. Pass on* settlement handlers to wire your UX to each outcome. Parameters
invoiceId
string
required
The invoice to collect payment for.
options
HandleSettlementOptions
Settlement callbacks dispatched after the mutation resolves — onChargedInline, onRequiresForm, onNoAction, etc. See the Settlement model.
The pay(input?) alias accepts an optional input:
input.idempotencyKey
string
Override the auto-generated Idempotency-Key.
input.forceForm
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.
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 === 0no_action
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>
  );
}
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.

See also

Settlement model

The full SettleOutcome union and handleSettlement dispatch behind usePayInvoice.

Payment methods

Capture a card before paying, or replace an in-use default.

Subscriptions

The subscriptions these invoices bill for.

Customer

payInvoice() on useCustomer mints a hosted-link alternative.