> 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

# Derivations and Formatting

> The pure functions that turn server facts into display models, and the Intl helpers behind them.

A derivation is a pure function from a hook's data to a display model: `(serverData, options) => ViewModel`. The drop-ins render from these, and they are exported from `@schematichq/schematic-components/elements` so your own JSX renders from the same answers. Numbers, names, and dates arrive formatted for `locale`, the raw values sit beside them, and none of them assemble sentences. Where a line needs wording, it carries a `kind` with its formatted parts, so the words and their translation stay in your code.

## The derivations

| Derivation                                                                                                                                         | Input                                                                      | Returns                                                                                                                                                                                                                                                             |
| -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `derivePlanManager({ company, featureUsage, creditBalances }, { locale, now?, showCredits?, showZeroPriceAsFree?, showWarningThresholdAsLimit? })` | `useCompany`, `useFeatureUsage`, `useCreditBalances`                       | `{ notice, plan, addOns, usageBased, planCredits, autoTopup, topUps, bundles, promotional, canChangePlan }`                                                                                                                                                         |
| `deriveIncludedFeatures(features, { locale, visibleFeatures?, showCredits?, showWarningThresholdAsLimit? })`                                       | `useFeatureUsage`                                                          | One row per feature: `featureId`, `name`, `description`, `icon`, `entitlement`, `usage`, `usageSummary`, `expiresAt`, `perLicenseCredits`, `hardLimit`, `tiers`, `source`                                                                                           |
| `deriveMeteredFeatures(features, { locale, visibleFeatures?, showCredits?, showWarningThresholdAsLimit? })`                                        | `useFeatureUsage`                                                          | One row per event or trait feature: `featureId`, `name`, `description`, `icon`, `headline`, `limit`, `resetsAt`, `meter`, `priceDetails`, `hardLimit`, `canAddMore`, `hasUsageByUser`, `unit`, `source`                                                             |
| `deriveCreditUsage(balances, { locale, visibleCredits? })`                                                                                         | `useCreditBalances`                                                        | One row per credit: `creditId`, `name`, `description`, `icon`, `remaining`, `purchasable`, `ledger`, `composition`, `unit`, `source`                                                                                                                                |
| `deriveCheckout({ catalog, cart, checkout?, company?, featureUsage?, isPricing?, display?, locale })`                                              | `useCatalog`, `useCompany`, `useCheckout`, `useFeatureUsage`               | `{ currency, period, plans, customPlan, downgradeContact, addOns, usage, addOnUsage, bundles, bundleMode, autoTopups, lines, totals, ... }`, the rows and summary the Checkout dialog renders                                                                       |
| `derivePricingTable(view, { locale, period?, currency?, company?, now?, showCredits?, showZeroPriceAsFree?, showWarningThresholdAsLimit? })`       | `useCatalog`, and `useCompany` when signed in                              | `{ source, periods, period, currencies, currency, plans, addOns, customPlan, savings, trialDaysLeft, heldAddOnIds }`; each card has `id`, `name`, `description`, `price`, `priceId`, `period`, `credits`, `entitlements`, `action`, `active`, `trialDays`, `source` |
| `checkoutSelectionFor(table, card, kind)`                                                                                                          | A `derivePricingTable` result, one of its cards, and `"plan"` or `"addOn"` | The `CheckoutSelection` for the card's action: the plan with its period and currency, or the add-ons the company would hold after it                                                                                                                                |
| `deriveUpcomingInvoice(invoice, { locale, format? })`                                                                                              | `useUpcomingInvoice`                                                       | `amountDueMinor`, `amountDueText`, `subtotalMinor`, `subtotalText`, `currency`, `dueAt`, `balanceApplied`, `balanceRemaining`, `discounts[]`                                                                                                                        |
| `derivePaymentMethods(methods, { locale, now? })`                                                                                                  | `usePaymentMethods`                                                        | `{ rows, current, others, monthsToExpiration, expiryWarning }`                                                                                                                                                                                                      |
| `deriveInvoiceList(page, { locale, format? })`                                                                                                     | `useInvoices`                                                              | `{ rows, count, hasMore }`; each row has `id`, `date`, `dateText`, `amountMinor`, `currency`, `amountText`, `isCredit`, `status`, `url`                                                                                                                             |
| `deriveUnsubscribe(company, featureUsage, { locale, now? })`                                                                                       | `useCompany`, `useFeatureUsage`                                            | `{ canUnsubscribe, accessEndsOn, period, plan, addOns, total }`                                                                                                                                                                                                     |

Most rows keep the server's row on `source`, so a component that needs a field the model leaves out reads it there instead of abandoning the derivation.

### Plan Manager

`derivePlanManager` lays the company out the way the [Plan Manager component](/components/drop-in/plan-manager) does. `notice` is at most one of four kinds, checked in order: `trial` with `endsIn` counted down in days, hours, or minutes; `canceled` with the plan name and date; `customPlanBilling` for a custom plan's pending invoice; and `scheduledDowngrade` with both plan names. `plan.price` is `{ kind: "amount", amount, period }`, `{ kind: "usageBased" }` for a free plan with usage-based features, or `{ kind: "free" }` when `showZeroPriceAsFree` is set. `usageBased` holds each feature with a price behavior, `planCredits` sums the plan's grants of each credit, and `topUps`, `bundles`, and `promotional` group the company's auto top-up, purchased, and free grants. `canChangePlan` is `false` while a custom plan waits for payment to activate.

### Included and metered features

`deriveIncludedFeatures` describes what each entitlement gives. `entitlement` is one of `units`, `perUnit`, `perPackage`, `tierUpTo`, `tierUnlimited`, `perUse`, or `unlimited`, each with its amounts and unit names formatted, or `null` for a feature with no entitlement line, such as a boolean feature. `usage` is a list of segments (`unitPricePerPeriod`, `used`, `cost`, `resets`) that the [Included Features component](/components/drop-in/included-features) joins with a bullet, and `usageSummary` stands in when that list is empty.

`deriveMeteredFeatures` keeps the event and trait features and gives each the parts of a usage card. `meter` has `value`, `total`, `percent` from 0 to 100, `valueText`, `totalText`, and a `tone` of `ok`, `warning`, `critical`, `tier`, or `overage`. A pay-as-you-go or credit-burndown feature has no meter. `hasUsageByUser` marks the event-based rows that `useFeatureUserUsage` can break down by user.

### Credits

`deriveCreditUsage` gives each credit its `remaining` amount and unit name, and a `ledger` of its live grants, newest first. Each ledger row has a `kind` of `plan`, `bundle`, `autoTopup`, or `promotional` for any other grant, the granted `amount` with the credit's name for it in `item`, its `createdAt` date, a `date` that says when the grant resets or expires, and the server's grant on `source`. `composition` spells out a per-license grant, such as 12 seats at 10 credits each.

### Pricing table

Selection state such as the period toggle stays in your component; `derivePricingTable` takes the selection and returns what the catalog offers. `periods` lists the periods the catalog prices, months before quarters before years, and `currencies` the currencies, lowercase. `period` and `currency` are the ones you asked for when the catalog offers them, else the subscription's period and the catalog's default currency, else the first; `period` is `null` when nothing recurs. Pass `company` from `useCompany` when signed in, so the current plan is current only in the period it bills in and the table opens on that period.

`plans` and `addOns` hold a card each, priced in that period and currency. A card with prices but none in the currency is left out, and a recurring one with none in the period; a one-time add-on is priced once. `price` is `{ kind: "amount", amount, period }`, `{ kind: "usageBased" }` for a plan priced at zero with a priced feature, or `{ kind: "free" }` with `showZeroPriceAsFree`. `priceId` is the price the card shows, for a checkout. `credits` lists the included credits the catalog shows, one line per credit; a credit granted per license and to the company outright reads as the per-license line with the company's share in `plus`. `savings` holds what the company's current plan saves by period against paying monthly, as a formatted percentage, and is empty in the public catalog. `entitlements` gives each feature a `text` with a `kind` of `name`, `unlimited`, `units`, `perUnit`, `perUse`, or `upTo`, a `detail` of `overage`, `tiered`, or `null`, its `tiers`, and its `hardLimit`.

`action` is `null` in the public catalog. In the company's view it is one of `current`; `choose` with `upgrade` for a plan above the current one; `remove` or `change` for a held add-on; `overLimit` with the `violations`; `downgradeBlocked` with the account's `text` and `url`; or `unavailable` when the company cannot check out. `active` marks the company's plan in the period shown, and `trialDaysLeft` counts down its trial. `customPlan` is the catalog's custom-plan call to action, with `priceText`, `ctaText`, and `ctaUrl`, or `null`.

### Checkout

`deriveCheckout` turns the catalog, the cart and the checkout the server last priced into what the [Checkout dialog](/components/drop-in/checkout) shows: the plan and add-on rows with their standing, the quantity, bundle and auto top-up rows, and the summary's lines and totals from `checkout.priceSnapshot`. The cart is its own small model: `initialCart({ catalog, company, config })` builds it from what the company holds and a `CheckoutConfig`, `cartReducer` applies each choice, and `checkoutSelections(cart, catalog, company)` turns it into the `CheckoutSelections` that `useCheckout().setSelections` writes, picking the price ids for the period and currency. `planSteps` derives the steps the cart has, and `stepOfProblem(problem, { cart, catalog })` names the step that can fix a problem, which is how a refused finalize lands the customer in the right place. `lines` are the cart's rows at the catalog's prices, the plan, each quantity bought ahead at its unit price, the add-ons and the bundles, and `totals` the snapshot's figures formatted, so a summary can show what makes up the total the server priced. The `autoTopups` rows name the credit and price one top-up on `costText`, which the catalog's own `autoTopups` do not. [Checkout API](/components/build-your-own/checkout-api) describes the resource these write to.

### Upcoming bill

`deriveUpcomingInvoice` formats the parts without doing the arithmetic, because the server sends how much balance the invoice consumes and how much survives. `balanceApplied` comes back negative, as a deduction, and `null` when none of the balance applies. Each entry in `discounts` has `couponName`, `code`, a `kind` of `percent` or `amount`, `valueText` such as "20%" or "\$5.00", and `months` for a discount that repeats, with the raw `percentOff`, `amountOffMinor`, and `duration` beside them.

### Invoices

`deriveInvoiceList` formats each row the way the [Invoices component](/components/drop-in/invoices) does: the due date, or the created date when there is none, in the locale's long form such as "March 1, 2026"; the amount as absolute text with `isCredit` carrying the sign; `currency` upper-cased; and `url` reduced to `null` unless it is an `http` or `https` link. `date` is `null` rather than an Invalid Date when the timestamp is unusable, and `dateText` is then empty.

### Payment methods

`derivePaymentMethods` lays the list out the way the [Payment Methods component](/components/drop-in/payment-methods) does. `current` is the default, or `null` when there is none, and `others` is every other row, or all of them when there is no default.

`expiryWarning` is `soon` when the default card has fewer than four months left, `expired` once its month has arrived, and `none` otherwise. Months count by calendar month and ignore the day, so a card expiring in August is two months away anywhere in June. `now` fixes the moment they are counted from, for a test or a server render.

Each row has `id`, `externalId`, `kind`, `label`, `icon`, `brand`, `type`, `last4`, `expiresShort`, `monthsToExpiration`, `expiry`, `isDefault`, and `canRemove`. `kind` is `card`, `bank`, `wallet`, or `other`. `icon` names the Schematic icon font glyph: `visa`, `mastercard`, `amex`, or `credit` for other cards, `bank` for a US bank account, the wallet's own mark, and `generic-payment` otherwise. `expiresShort` is the card's "8/27".

`label` is either `{ key }`, copy to translate such as `paymentMethodsCardEndingIn` for "Card ending in", or `{ text }`, a value the provider supplied such as a bank's name or the email behind a Link account. That keeps the translatable words in your strings:

```ts
const { current, others } = derivePaymentMethods(data, { locale: "en-US" });
const name = (row: PaymentMethodRow) =>
  row.label.key !== undefined ? t(row.label.key) : row.label.text;
```

### Cancellation

`deriveUnsubscribe` answers what a cancel dialog needs. `canUnsubscribe` is true while there is a subscription with no cancellation date. `accessEndsOn` is the cancellation date, else the next bill date, else today. `plan`, `addOns`, and `total` are what cancelling stops charging for, with `total` the recurring cost per `period`.

### Overriding one field

`deriveInvoiceList` and `deriveUpcomingInvoice` take `format`, which replaces one formatter at a time and receives the raw value. The `amount` formatter receives the signed amount, unlike `deriveInvoiceList`'s default text, which is absolute:

```ts
deriveInvoiceList(page, {
  locale: "en-US",
  format: {
    date: (date, locale) => date.toLocaleDateString(locale),
    amount: (amountMinor, currency, locale) => myMoney(amountMinor, currency),
  },
});
```

## Formatting helpers

| Helper                                                                                                            | Does                                                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `formatCurrency(amountMinor, currency, locale, options?)`                                                         | Currency with zero-decimal handling and significant-digit switching for small unit prices. `{ preserveSubUnitPrecision: false }` rounds to the currency's usual precision. |
| `formatNumber(value, locale, options?)`                                                                           | Grouped numbers, with `Intl.NumberFormat` options passed through                                                                                                           |
| `formatPercent(fraction, locale)`                                                                                 | "50%", or "12.5%" for a fractional percentage                                                                                                                              |
| `formatDate(date, locale, options?)`, `formatShortDate(date, locale)`, `formatShortDateRange(start, end, locale)` | A `Date` as "August 21, 2026" or "8/21", or a range as "8/21 – 9/20". Empty for an Invalid Date.                                                                           |
| `formatMonthYear(month, year, locale)`                                                                            | A card's expiry as the locale writes a month and year                                                                                                                      |
| `formatConsumptionRate(rate, locale)`                                                                             | Rates readable down to ten decimal places                                                                                                                                  |
| `plural(locale, count, forms)`                                                                                    | Picks from `{ one?, other, … }` by the locale's plural category                                                                                                            |
| `featureName({ name, singularName?, pluralName? }, count?, locale?)`                                              | Singular or plural feature or credit name, using configured forms first                                                                                                    |
| `resolveLocale(locale?)`, `viewerLocale()`                                                                        | The tag or `en-US` when it is absent or `Intl` rejects it; the browser language, if usable                                                                                 |
| `httpStatus(error)`                                                                                               | The HTTP status behind a failed billing read, or `undefined` for anything that was not an HTTP response                                                                    |

`usageLimit` and `currentTier` read a single `FeatureUsage` row directly, and `pricePeriod`, `tierRanges`, `isTieredPrice`, and `unitPrice` read its `price`, for a component that works from the rows rather than a derivation. `<Icon name={row.icon} />` draws a feature's or credit's icon the way the drop-ins do: the glyph for a schematic-icons name, and anything else, such as an emoji, as text.

## Tests travel with them

The drop-ins render from the same derivations, and their test suites check the derivations directly, so a rendering rule that changes is caught before it reaches a page. Pin `@schematichq/schematic-components` to a minor to take those changes deliberately.