> If you are setting up, configuring, or answering questions about Schematic, connect the
> Schematic MCP server before working from these pages. It reads and writes plans, features,
> entitlements, and overrides against the real account, so what you report and what you build
> match what actually exists.
>
> The server is at https://api.schematichq.com/mcp
>
> From Claude, ChatGPT, or another chat assistant, add it as a custom connector:
> https://docs.schematichq.com/working-with-ai
>
> From a coding assistant such as Claude Code, Codex, or Cursor:
> https://docs.schematichq.com/building-with-ai
>
> For clean Markdown of any page, append `.md` to the page URL. For a complete page index,
> see https://docs.schematichq.com/llms.txt

# Hooks Reference

> One hook per billing resource from @schematichq/schematic-react. Signatures, return shapes, and what the data means.

Every read hook returns a handle with the same shape and reads through the session on `SchematicProvider`:

```ts
interface ResourceHandle<T> {
  data: T | undefined;      // undefined until the first successful load; retained across later errors
  error: Error | undefined; // the latest failure; cleared when the next request starts
  isPending: boolean;       // a request is in flight and nothing has arrived yet, or a refetch is running
  refetch: () => void;
}
```

The three states a component renders follow from those fields. `data === undefined && isPending` is the first load. `data === undefined && error` is a failure with nothing to show. `data !== undefined` is ready, and a set `error` alongside it is a failure to report without replacing what is there.

Mounted outside any provider, every hook reports the missing source on its `error` rather than throwing, so a misconfigured page fails the same way a request would. Under a provider whose `session` is still `undefined`, the hooks stay pending instead, apart from [`useCatalog`](#usecatalog), which reads the public catalog meanwhile. Under `session={null}` they settle empty, again apart from `useCatalog`. The hooks never fetch during server rendering: without `initialData` they report pending on the server and load on the client.

The company endpoints, `GET /company` and the `/company/*` reads under it, are behind the `company-context-api` flag, per account, and answer 404 `{"error":"Not found"}` until it is on. A 404 there is the flag or the token, not a company that could not be found; ask Schematic to enable the flag for the account. Hooks surface it as an error rather than an empty answer, so a page cannot quietly report "nothing here" over real data. `httpStatus(error)` from `@schematichq/schematic-components/elements` reads the status off a billing error, and returns `undefined` for anything that was not an HTTP response.

The hooks are also re-exported from `@schematichq/schematic-components/elements`, so a file that imports a derivation can take its hook from the same place. Timestamps on every resource arrive as `Date` objects.

## useCheckout

```ts
import type { Checkout, CheckoutPriceSnapshot, CheckoutProblem, CheckoutResult, CheckoutSelections } from "@schematichq/schematic-react";

declare function useCheckout(options?: { checkoutId?: string }): CheckoutHandle;

interface CheckoutHandle {
  checkout: Checkout | undefined;      // as the last write left it: id, version, status, selections, priceSnapshot, problems, expiresAt
  snapshot: CheckoutPriceSnapshot | undefined;
  problems: CheckoutProblem[];         // the priced checkout's, or the list a refused finalize answered with
  isPricing: boolean;                  // a write is in flight or waiting behind one
  isFinalizing: boolean;
  error: Error | undefined;            // a write or finalize that failed other than on its problems
  result: CheckoutResult | undefined;  // what the finalize charged
  setSelections(selections: CheckoutSelections): Promise<Checkout | undefined>;
  finalize(): Promise<CheckoutResult>;
  reset(): void;
}
```

A persisted checkout on `POST /checkouts`, `PUT /checkouts/{checkout_id}` and `POST /checkouts/{checkout_id}/finalize`. The first `setSelections` opens it and every later one replaces the cart and re-prices on the server; rapid calls fold into the newest, a stale version is replayed once, and `finalize` charges under the session the last price handed out, then reloads every billing resource. A refused finalize puts the server's list on `problems` and rejects, and `checkoutProblemsOf(error)` returns that list. `{ checkoutId }` resumes a checkout, which js 1.16.1 and later read back before the first write. While `isPricing` is true, `snapshot` and `problems` are still the previous cart's, so dim them until the write lands rather than show the old plan's price under the new plan's name. The drop-in dialog opens through [`useCheckoutLauncher`](#usecheckoutlauncher). [Checkout API](/components/build-your-own/checkout-api) describes the resource and the wire shapes.

## useCheckoutLauncher

```ts
function useCheckoutLauncher(): { open(config?: CheckoutConfig): void; close(): void; isOpen: boolean };
```

Exported from `@schematichq/schematic-components/elements` alone, beside the dialog it opens. `CheckoutLauncherProvider` renders one `<Checkout />` and `useCheckoutLauncher()` below it opens that dialog on `config` (`selection`, `steps`, `display`, `catalogId`), each section laid over the provider's `defaults`; every open starts a new cart. Outside a provider `open` does nothing and says so in development. [Checkout](/components/drop-in/checkout#opening-it-from-anywhere) shows the pattern.

## useTaxIds

```ts
function useTaxIds(): ResourceHandle<TaxId[]> & {
  update(taxId: { type: TaxIdType; value: string }): Promise<void>;
  isMutating: boolean;
  mutationError: Error | undefined;
};
```

The tax IDs on the company's billing customer, read from `GET /checkout/tax-id`: each has an `id`, its provider `type` such as `eu_vat` or `us_ein`, `value`, `country` and `verificationStatus`. `data` is empty when there are none. `update` posts one to `POST /checkout/tax-id` and reloads the list; it rejects with the failure and records it on `mutationError`. A finalize reads the tax ID from the billing customer rather than from the cart, so save it first, as the drop-in does when the field is left.

## useCompany

```ts
function useCompany(): ResourceHandle<Company>;
```

The company and what it is subscribed to, read from `GET /company`:

| Field                | Meaning                                                                                                                                                                |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`, `name`         | The company.                                                                                                                                                           |
| `plan`               | The current plan: `id`, `name`, `description`, `price` in minor units, `period`, and `includedCreditIds`. Absent when the company has no plan.                         |
| `addOns`             | Each add-on the company holds, in the same shape as `plan`.                                                                                                            |
| `subscription`       | `status`, `currency`, `period`, `periodEnd`, `nextBillAt`, `trialEnd`, `cancelAt`, and `cancelAtPeriodEnd`. Absent when the company has none, and when it has several. |
| `scheduledDowngrade` | `fromPlanId`, `fromPlanName`, `toPlanId`, and `toPlanName` for a downgrade that takes effect at period end.                                                            |
| `customPlanBilling`  | The newest pending custom plan invoice: `planId`, `planName`, `dueAt`, `invoiceUrl`, and `activationStrategy`.                                                         |

[Plan Manager](/components/drop-in/plan-manager) and the [Unsubscribe Button](/components/drop-in/unsubscribe-button) read this, and so does the [Pricing Table](/components/drop-in/pricing-table) once a session starts.

## useCatalog

```ts
function useCatalog(options?: { catalogId?: string }): ResourceHandle<CatalogView>;

type CatalogView =
  | { source: "company"; catalog: Catalog }
  | { source: "public"; catalog: PublicCatalog };
```

Plans, add-ons, and credit bundles with prices per period and currency, tier shapes, and entitlement summaries, for the environment's catalog or the one `catalogId` names. The hook keys by value, so an inline object literal is fine.

The source follows the session. With a session it reads the company's view from `GET /catalog/view` (or `/catalogs/:catalog_id/view`), which adds each plan's and add-on's standing: `current`, `valid` with an `invalidReason` and `usageViolations` when false, and `companyCanTrial`, plus `capabilities.checkout` for whether the company can check out at all. With no session, or while the session is still `undefined`, it reads `GET /public/catalog` (or `/public/catalogs/:catalog_id`) with the provider's publishable key, so unlike the other hooks it does not wait on your auth. A session starting swaps the public catalog for the company's view. `data.source` says which one you have; the public catalog carries no standing, so check it before reading `current` or `valid`.

Each plan's `autoTopups` lists its included credits that top up: `planCreditGrantId`, which a checkout's `autoTopupOverrides` name, `creditId`, the plan's `amount`, `thresholdCredits` and `enabled`, the company's own `companyAmount`, `companyThresholdCredits` and `companyEnabled` where `availability` is `user_controlled`, and `unitPrice`, the credit's price a top-up is charged at. It carries no credit name: take it from the plan's `includedCreditGrants` entry with the same `creditId`, or let [`deriveCheckout`](/components/build-your-own/derivations#checkout) do both, since its `autoTopups` rows carry `creditName` and `costText`, the cost of one top-up at `amount` times the unit price.

A 404 from either means the account is not enabled for catalogs (the `multi-product-catalog` flag), or that `catalogId` names no catalog of the account's. The [Pricing Table](/components/drop-in/pricing-table) reads this, and [`derivePricingTable`](/components/build-your-own/derivations#pricing-table) takes it whole.

## useFeatureUsage

```ts
function useFeatureUsage(): ResourceHandle<FeatureUsage[]>;
```

One row per feature the company is entitled to, read from `GET /company/usage`. An empty list is a loaded answer that means the company is entitled to nothing.

| Field                                                           | Meaning                                                                                                                                                                                                |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `featureId`, `featureName`, `featureDescription`, `featureIcon` | The feature, with `featureSingularName` and `featurePluralName` where they are configured and `""` where they are not, so fall back on `featureName` for an empty string as well as for a missing one. |
| `featureType`                                                   | `boolean`, `event`, `trait`, or `license`.                                                                                                                                                             |
| `access`                                                        | Whether the company has access to the feature.                                                                                                                                                         |
| `entitlementType`, `planEntitlementId`, `companyOverrideId`     | `plan_entitlement` or `company_override`, with the id of whichever it is. Only an override carries `expiresAt`.                                                                                        |
| `valueType`, `valueNumeric`, `valueBool`                        | The entitlement's value: `boolean`, `numeric`, `unlimited`, `trait`, or `credit`.                                                                                                                      |
| `allocation`, `usage`, `softLimit`, `warningThreshold`          | The limit, the usage against it, an overage's soft limit, and the warning threshold.                                                                                                                   |
| `priceBehavior`                                                 | `pay_in_advance`, `pay_as_you_go`, `overage`, `tier`, or `credit_burndown`. Absent when the feature is not priced.                                                                                     |
| `price`, `currentCost`                                          | The metered price the company is billed at, tiers included, and what the feature costs at that price, in minor units.                                                                                  |
| `metricPeriod`, `metricPeriodMonthReset`, `resetsAt`            | The usage period and when it next resets.                                                                                                                                                              |
| `creditId`, `creditName`, `creditIcon`, `consumptionRate`       | The credit a credit-burndown feature draws on, with `creditSingularName` and `creditPluralName` where they are configured, and how much one use costs.                                                 |
| `licenseId`, `perLicenseCreditGrants`                           | The license and the plan's per-license credit grants, on a license feature.                                                                                                                            |

[Included Features](/components/drop-in/included-features), [Metered Features](/components/drop-in/metered-features), [Plan Manager](/components/drop-in/plan-manager), and the [Unsubscribe Button](/components/drop-in/unsubscribe-button) all read this, so a page showing all four makes one request.

## useFeatureUserUsage

```ts
function useFeatureUserUsage(featureId: string): ResourceHandle<FeatureUserUsage>;
```

One event-based feature's usage by user over its metric period, read from `GET /company/usage/:feature_id/users`. `users` holds up to `FEATURE_USER_USAGE_LIMIT` (20) of the heaviest users, each with `userId`, `name`, `usage`, `share`, and `lastSeen`. `count` is how many users there are in all, `total` is the period's usage, `startTime` and `endTime` bound the period, and `unattributed` is usage from events sent without a user. A feature that is not an event-based entitlement of the company answers 404.

## useCreditBalances

```ts
function useCreditBalances(): ResourceHandle<CreditBalanceEntry[]>;
```

One balance per credit, read from `GET /company/credits`. An empty list is a loaded answer that means the company holds no credits and its plan draws on none.

| Field                                                       | Meaning                                                                                                                                                                                                                    |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `creditId`, `creditName`, `creditDescription`, `creditIcon` | The credit, with `creditSingularName` and `creditPluralName` where they are configured.                                                                                                                                    |
| `total`, `used`, `remaining`                                | Across live grants, so `total` is `used` plus `remaining`.                                                                                                                                                                 |
| `resetsAt`, `expiresAt`                                     | When the balance resets or runs out.                                                                                                                                                                                       |
| `purchasable`                                               | Whether a bundle of the credit is on offer to the company's plan.                                                                                                                                                          |
| `autoTopup`                                                 | `enabled`, `selfService`, `amount`, and `thresholdCredits`.                                                                                                                                                                |
| `composition`                                               | How a per-license plan grant adds up per period: `licenseId`, `licenseName` with `licenseSingularName` and `licensePluralName`, `licenseQuantity`, `perLicenseAmount`, `fixedQuantity`, `total`, `period`, and `renewsAt`. |
| `grants`                                                    | Live grants, newest first, each with `id`, `grantReason`, `quantity`, `quantityUsed`, `quantityRemaining`, `createdAt`, `validFrom`, `renewalPeriod`, `resetsAt` or `expiresAt`, and the plan or bundle behind it.         |

A credit the plan draws on that the company holds none of is listed with a zero balance and no grants. Expired and zeroed-out grants leave the list on the server.

## useCreditUserUsage

```ts
function useCreditUserUsage(creditId: string): ResourceHandle<CreditUserUsage>;
```

One credit's consumption by user over the span of its live grants, read from `GET /company/credits/:credit_id/users`. `users` holds up to `FEATURE_USER_USAGE_LIMIT` (20) of the heaviest users, each with `userId`, `name`, `used`, and `share`, alongside `count`, `total`, `startTime`, `endTime`, and `unattributed`. A credit the company holds no live grant of comes back empty; an unknown credit answers 404.

## useUpcomingInvoice

```ts
function useUpcomingInvoice(): ResourceHandle<UpcomingInvoice | null>;
```

The next bill: `amountDue` and `subtotal` in minor units, `currency`, `dueDate`, the active `discounts`, and `customerBalanceApplied` and `customerBalanceRemaining` computed on the server. `null` is a loaded answer that means nothing to bill; only `undefined` means the resource has not loaded.

## useInvoices

```ts
function useInvoices(query?: InvoiceQuery): ResourceHandle<InvoicePage> & { loadMore: () => Promise<void> };

type InvoiceQuery = { includePending?: boolean };
type InvoicePage = { invoices: Invoice[]; count: number; hasMore: boolean };
```

The invoice history, `INVOICE_PAGE_SIZE` (12) invoices a page. `count` is how many invoices the query matches on the server, so a list showing four rows can say how many the company has. `hasMore` is that count set against the rows loaded, and an empty page ends the list regardless. `loadMore` appends the next page, settles when it lands, and never rejects; read `error` for the failure, and the rows already fetched stay put. `isPending` is true while the page is on the wire, so a button can disable itself without awaiting anything, and clicking twice makes one request. `refetch` re-requests every page loaded so far, not only the first.

`{ includePending: true }` includes unpaid invoices that are not yet due. Each distinct query is its own list with its own paging, so `useInvoices({ includePending: true })` and `useInvoices()` never share rows. The hook keys by value, so an inline object literal is fine.

An empty history is a successful response with `count: 0`. A 404 surfaces on `error`, so a page cannot report "no invoices" over a real failure.

Each `Invoice`:

| Field       | Type                              | Meaning                                                                                         |
| ----------- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
| `id`        | `string`                          | The invoice's identifier.                                                                       |
| `amountDue` | `number`                          | Signed, in the currency's minor units. A negative amount is a credit note.                      |
| `currency`  | `Currency`                        | Lowercase ISO 4217 code, such as `usd`.                                                         |
| `createdAt` | `Date`                            | When the invoice was created.                                                                   |
| `dueDate`   | `Date \| null`, optional          | When it is due, if the billing provider set one.                                                |
| `status`    | `InvoiceStatus \| null`, optional | `open` or `paid`. The server leaves draft, uncollectible, and void invoices out of the history. |
| `url`       | `string \| null`, optional        | The hosted invoice page, if there is one.                                                       |

The optional fields arrive as `null` or are absent, so read them with `??` rather than a strict `null` check.

```tsx
import { useInvoices } from "@schematichq/schematic-react";

function InvoiceHistory() {
  const { data, error, isPending, loadMore, refetch } = useInvoices();

  if (data === undefined) {
    return error ? <button onClick={refetch}>Retry</button> : <p>Loading…</p>;
  }
  if (data.invoices.length === 0) {
    return <p>No invoices yet</p>;
  }
  return (
    <>
      <ul>
        {data.invoices.map((invoice) => (
          <li key={invoice.id}>
            <a href={invoice.url ?? undefined}>
              {(invoice.dueDate ?? invoice.createdAt).toLocaleDateString()}
            </a>{" "}
            {invoice.amountDue / 100} {invoice.currency.toUpperCase()}
          </li>
        ))}
      </ul>
      {data.hasMore && (
        <button disabled={isPending} onClick={loadMore}>
          Load more
        </button>
      )}
      {error && <p role="alert">{error.message}</p>}
    </>
  );
}
```

## usePaymentMethods

```ts
function usePaymentMethods(): ResourceHandle<PaymentMethod[]> & {
  setDefault: (externalId: string) => Promise<void>;
  remove: (id: string) => Promise<void>;
  isMutating: boolean;              // a write is in flight; distinct from isPending, which is the list loading
  mutationError: Error | undefined; // the last write's failure; cleared when the next write starts
};
```

Every payment method on file. A company with none is a successful response with an empty list, so an empty `data` is a loaded answer. A 404 surfaces on `error`.

`setDefault` and `remove` each reload the list once they land, and reject with the failure while also recording it on `mutationError`, so a component can await the promise or read the field, whichever it prefers. A write's failure never lands on `error`, which stays the list's own. `setDefault` takes the method's `externalId`, the billing provider's id; `remove` takes Schematic's `id`.

Each `PaymentMethod`:

| Field                                                   | Type      | Meaning                                                                                                                                             |
| ------------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                                    | `string`  | Schematic's identifier. `remove` takes this.                                                                                                        |
| `externalId`                                            | `string`  | The billing provider's identifier. `setDefault` takes this.                                                                                         |
| `type`                                                  | `string`  | The provider's payment method type, such as `card`, `us_bank_account`, or `link`.                                                                   |
| `isDefault`                                             | `boolean` | The method the company is billed with: the subscription's own when it has one, else the customer's default. At most one is `true`, and none may be. |
| `canRemove`                                             | `boolean` | Whether `remove` would be accepted. `false` only for the last method while a paid subscription that is active, trialing, or past due needs it.      |
| `cardBrand`, `cardLast4`, `cardExpMonth`, `cardExpYear` | optional  | The card behind a `card` method, which is also how Stripe saves an Apple Pay or Google Pay card.                                                    |
| `bankName`, `accountLast4`                              | optional  | A bank account's bank and last four digits.                                                                                                         |
| `accountName`, `billingName`, `billingEmail`            | optional  | The names and email the provider supplied, such as the email behind a Link account.                                                                 |

The optional fields arrive as `null` or are absent, so read them with `??` rather than a strict `null` check.

The server decides both flags, so a component never re-derives the provider's rules. Removing the default promotes nothing; the list then has no default until someone sets one. Call `setDefault` with a new method's id once Stripe confirms it; the server only makes a customer's first method the default.

```tsx
import { usePaymentMethods } from "@schematichq/schematic-react";

function Methods() {
  const { data, isMutating, mutationError, remove, setDefault } = usePaymentMethods();
  if (data === undefined) return <p>Loading…</p>;

  return (
    <>
      <ul>
        {data.map((method) => (
          <li key={method.id}>
            {method.cardBrand ?? method.type} {method.cardLast4 ?? method.accountLast4}
            {method.isDefault ? (
              " (default)"
            ) : (
              <button disabled={isMutating} onClick={() => setDefault(method.externalId).catch(() => {})}>
                Set default
              </button>
            )}
            {method.canRemove && (
              <button disabled={isMutating} onClick={() => remove(method.id).catch(() => {})}>
                Remove
              </button>
            )}
          </li>
        ))}
      </ul>
      {mutationError && <p role="alert">{mutationError.message}</p>}
    </>
  );
}
```

## useSetupIntent

```ts
function useSetupIntent(): { create: () => Promise<SetupIntent> };

type SetupIntent = {
  setupIntentClientSecret?: string | null;
  publishableKey?: string | null;  // the account's own Stripe key, for a direct account
  schematicPublishableKey: string; // Schematic's key, for a connected account
  accountId?: string | null;       // the connected account, when there is one
};
```

Mints a Stripe SetupIntent for adding a payment method. Each call makes a new intent, because an intent is single-use. The hook holds no state; `create` rejects with the failure.

Load Stripe with `schematicPublishableKey` and `stripeAccount: accountId` when `accountId` is set, else with `publishableKey`. Mount Stripe's Payment Element on the client secret, confirm the setup, then make the new method the default with `usePaymentMethods().setDefault`, which also reloads the list. [Checkout with Stripe Elements](/components/build-your-own/checkout-with-stripe-elements) walks through the Stripe side.

```tsx
import { usePaymentMethods, useSetupIntent, type SetupIntent } from "@schematichq/schematic-react";
import { Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
import { loadStripe, type Stripe } from "@stripe/stripe-js";
import { useEffect, useState, type FormEvent } from "react";

// A connected account is reached through Schematic's key with the account
// named; a direct account uses its own key.
function stripeFor(intent: SetupIntent): Promise<Stripe | null> {
  return intent.accountId
    ? loadStripe(intent.schematicPublishableKey, { stripeAccount: intent.accountId })
    : loadStripe(intent.publishableKey ?? intent.schematicPublishableKey);
}

function AddPaymentMethod({ onDone }: { onDone: () => void }) {
  const { create } = useSetupIntent();
  // Stripe is loaded once per intent: <Elements> must keep the same promise.
  const [ready, setReady] = useState<{ clientSecret: string; stripe: Promise<Stripe | null> }>();
  const [error, setError] = useState<Error>();
  useEffect(() => {
    create().then(
      (intent) => {
        if (intent.setupIntentClientSecret) {
          setReady({ clientSecret: intent.setupIntentClientSecret, stripe: stripeFor(intent) });
        } else {
          setError(new Error("The payment form could not be loaded."));
        }
      },
      (failure: Error) => setError(failure),
    );
  }, [create]);

  if (error) return <p role="alert">{error.message}</p>;
  if (!ready) return <p>Loading…</p>;
  return (
    <Elements stripe={ready.stripe} options={{ clientSecret: ready.clientSecret }}>
      <SaveForm onDone={onDone} />
    </Elements>
  );
}

function SaveForm({ onDone }: { onDone: () => void }) {
  const stripe = useStripe();
  const elements = useElements();
  const { setDefault } = usePaymentMethods();

  async function save(event: FormEvent) {
    event.preventDefault();
    if (!stripe || !elements) return;
    const { setupIntent, error } = await stripe.confirmSetup({
      elements,
      confirmParams: { return_url: window.location.href },
      redirect: "if_required",
    });
    const id = setupIntent?.payment_method;
    if (!error && typeof id === "string") {
      await setDefault(id); // the new method becomes the default, and the list reloads
      onDone();
    }
  }

  return (
    <form onSubmit={save}>
      <PaymentElement />
      <button type="submit">Save payment method</button>
    </form>
  );
}
```

## useUnsubscribe

```ts
function useUnsubscribe(): {
  unsubscribe: () => Promise<void>;
  isMutating: boolean;              // the cancellation is in flight
  mutationError: Error | undefined; // the last attempt's failure; cleared when the next one starts
};
```

Sets the company's subscription to cancel at the end of its billing period. Once it lands, whichever of the company, the upcoming invoice, and the credit balances are loaded reload, so `useCompany` shows the cancellation date on its next render. `unsubscribe` rejects with the failure and records it on `mutationError`. The server refuses when the company has no subscription, when the subscription is already set to cancel at period end, or when another Stripe app manages it.

```tsx
import { useCompany, useUnsubscribe } from "@schematichq/schematic-react";

function CancelLink() {
  const { data: company } = useCompany();
  const { unsubscribe, isMutating, mutationError } = useUnsubscribe();
  if (!company?.subscription || company.subscription.cancelAt) return null;

  return (
    <>
      <button disabled={isMutating} onClick={() => unsubscribe().catch(() => {})}>
        Cancel at period end
      </button>
      {mutationError && <p role="alert">{mutationError.message}</p>}
    </>
  );
}
```

## useInvalidateBillingData

```ts
function useInvalidateBillingData(): () => void;
```

Returns a function that reloads every loaded billing resource while keeping its current data on screen. Call it after an action that changes what the company is billed for, such as a plan change your own backend makes.

```tsx
import { useInvalidateBillingData } from "@schematichq/schematic-react";

function UpgradeButton({ changePlan }: { changePlan: () => Promise<void> }) {
  const invalidate = useInvalidateBillingData();
  return <button onClick={async () => { await changePlan(); invalidate(); }}>Upgrade</button>;
}
```

## Locale and copy

The drop-ins format and word everything through the `locale`, `strings`, and `translate` on `SchematicProvider`. These read the same settings, so a component of your own follows the same overrides. [Localization](/components/drop-in/localization) covers the settings themselves.

```ts
// @schematichq/schematic-react
function useSchematicI18n(): { locale?: string; translate?: Translate; strings?: StringOverrides; onMissingString?: (key: string) => void };
function useSchematicLocale(): string | undefined;
function useSchematicTranslate(): Translate | undefined;
function useSchematicStrings(): StringOverrides | undefined;

// @schematichq/schematic-components/elements
function useResolvedLocale(locale?: string): string;
function useTranslator(strings?: StringOverrides, locale?: string): (key: StringKey, vars?: StringVars) => string;
```

The `useSchematic*` hooks return the provider's settings as given: `useSchematicLocale` is `undefined` when the provider sets no locale, and outside any provider `useSchematicI18n` returns an empty object. `useResolvedLocale` applies the drop-ins' fallback: the argument, else the provider's locale, else the viewer's. The viewer's language is left out of a server render and its hydration, so set `locale` on the provider for a server-rendered page.

`useTranslator` returns the function the drop-ins word their copy with. It looks a key up in the `strings` you pass, then the provider's `strings`, then the provider's `translate`, then the English default, so it always returns a string. The [customer portal recipe](/components/build-your-own/recipes#customer-portal) uses it to word a payment method's label.

`SchematicI18nProvider` sets `locale`, `translate`, `strings`, and `onMissingString` for the part of the tree below it. `SchematicProvider` renders one, so you need it only to change the settings for one part of a page. A nested provider merges with the one above it: one that sets only `locale` keeps the `translate` above it, and `strings` combine key by key.

## Testing and fixtures

`BillingDataProvider` serves fixed data to the hooks with no network and no session, so a component can be rendered in a test or a story with the rows you choose.

```tsx
import { BillingDataProvider } from "@schematichq/schematic-react";

<BillingDataProvider data={{ invoices: { invoices: [], count: 0, hasMore: false } }}>
  <InvoiceHistory />
</BillingDataProvider>;
```

`data` takes a value per resource: `catalog`, `publicCatalog`, `company`, `featureUsage`, `creditBalances`, `upcomingInvoice`, `paymentMethods`, and `invoices`. A missing key reports as pending. Given `publicCatalog` without `catalog`, `useCatalog` reports the public source, as on a signed-out page. The per-user breakdowns are not part of `data`: they have their own props, `featureUserUsage` and `creditUserUsage`, each keyed by feature or credit id. `status` overrides a resource's `isPending` or `error`, so the loading and failure states render on demand, and `onRefetch` and `onLoadMoreInvoices` receive the calls a component makes. `actions` stands in for the writes: `setDefaultPaymentMethod`, `removePaymentMethod`, `createSetupIntent`, and `unsubscribe` receive the calls `usePaymentMethods`, `useSetupIntent`, and `useUnsubscribe` make, and whatever they resolve or reject with is what the component sees. The provider also accepts `locale`, `strings`, `translate`, and `onMissingString`, so a fixture can exercise copy overrides too.

## Flags and entitlement checks

The hooks above read billing state. Feature flags and entitlement checks keep their own hooks on the same provider: `useSchematicFlag`, `useSchematicEntitlement`, `useSchematicIsPending`, `useSchematicPlan`, `useSchematicCreditBalance`, `useSchematicCreditSpendPolicies`, and `useSchematicEvents`. See the [React SDK](/developer_resources/sdks/react).