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

# Catalog & pricing

> Read the merchant's live plans, products, public pricing matrix, and branding — for the org in context.

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

Read-only hooks for the merchant catalog customers can see: the flat live-plan list, the active product catalog, the full public pricing matrix for one product, and the org's branding. Customers only ever see active plans and products — draft, archived, and migrated rows never surface.

<LearnHooksTip />

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

## usePlans

Returns the merchant's live plans — the flat list from `GET /v1/plans?status=active`. Customers only ever see active plans (never draft, archived, or migrated). For a customer-facing pricing page with tiers, intervals, entitlements, credit packages, and addons, use `usePricing` instead.

**Parameters** — none.

**Returns**

* `plans` — `Plan[]` — the active plans, or `[]` while loading. Each plan:

<ParamField body="id" type="string">Plan identifier.</ParamField>
<ParamField body="name" type="string">Display name.</ParamField>
<ParamField body="slug" type="string">Stable slug.</ParamField>
<ParamField body="description" type="string | null">Optional description.</ParamField>
<ParamField body="isPublic" type="boolean">Whether the plan is publicly listed.</ParamField>
<ParamField body="metadata" type="Record<string, unknown>">Arbitrary merchant metadata.</ParamField>

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

export default function PlanList() {
  const { plans, isLoading, error } = usePlans();

  if (isLoading) return <div>Loading…</div>;
  if (error) return <div>Couldn’t load plans.</div>;

  return (
    <ul>
      {plans.map((plan) => (
        <li key={plan.id}>
          <strong>{plan.name}</strong>
          {plan.description ? ` — ${plan.description}` : null}
        </li>
      ))}
    </ul>
  );
}
```

## useProducts

Returns the org's active product catalog — read-only, from `GET /v1/products?status=active`. Customers only ever see live products; archived ones are tombstones and never surface. This also powers `usePricing`'s single-product auto-derive fallback: when there's exactly one active product, a portal with no explicit `productId` self-resolves to it.

**Parameters** — none.

**Returns**

* `products` — `Product[]` — the active products, or `[]` while loading. Each product:

<ParamField body="id" type="string">Product identifier.</ParamField>
<ParamField body="organizationId" type="string">Owning org.</ParamField>
<ParamField body="name" type="string">Display name.</ParamField>
<ParamField body="slug" type="string">Stable slug.</ParamField>
<ParamField body="description" type="string | null">Optional description.</ParamField>
<ParamField body="imageUrl" type="string | null">Optional product image.</ParamField>
<ParamField body="status" type="'active' | 'archived'">Always `active` from this hook.</ParamField>
<ParamField body="metadata" type="Record<string, unknown>">Arbitrary merchant metadata.</ParamField>
<ParamField body="createdAt" type="string">ISO timestamp.</ParamField>
<ParamField body="updatedAt" type="string">ISO timestamp.</ParamField>

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

export default function ProductPicker() {
  const { products, isLoading, error } = useProducts();

  if (isLoading) return <div>Loading…</div>;
  if (error) return <div>Couldn’t load products.</div>;

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}
```

## usePricing

Returns the complete customer-facing catalog for one product from `GET /v1/public/pricing/:productId`: tiers, plans-per-tier (interval variants), entitlements, credit packages, addons, and a canonical top-level `features[]` row list for building a pricing matrix. The server filters to self-serve tiers automatically. This is the hook customer-facing pricing pages want — not `usePlans`, the flat live-plan list.

**Parameters**

<ParamField body="productId" type="string">
  The product to load pricing for. **Optional** — when omitted, it's derived in this order:

  1. Explicit arg — `usePricing('prod_xxx')`
  2. `UnitPayConfig.productId` (the escape hatch set on `<UnitPayProvider>`)
  3. The active subscription's `productId` (the sub *is* the answer)
  4. A single-product org auto-resolves (`useProducts()` length === 1)
  5. Otherwise `null` → returns `{ pricing: null, isLoading: false, error: null }` (multi-product org with no sub — pass `productId` explicitly)

  While the derivation inputs (subscription, products) are still loading, `isLoading` stays `true`.
</ParamField>

**Returns**

* `pricing` — the `PricingResponse` matrix, or `null` when no product could be resolved / while loading:

<ParamField body="currency" type="string">Catalog display currency.</ParamField>
<ParamField body="product" type="object">`{ id, name, slug, description, imageUrl }`.</ParamField>
<ParamField body="features" type="PricingFeature[]">Canonical matrix row order — the union of every entitlement slug across every plan in every tier, sorted by `displayOrder`. Each row carries `slug`, `name`, `eventName` (billable metric, or `null`), and `creditCurrencyId` (wallet it debits, or `null`).</ParamField>
<ParamField body="tiers" type="PricingTier[]">The pricing columns. Each tier carries `plans[]` (interval variants), `addons[]`, `creditPackages[]`, an `entitlementBySlug` lookup (branches on `mode`: `unlimited` / `quota` / `rate`), plus presentation flags (`isPopular`, `isForeverFree`, `isSelfServe`, `isSalesLed`, `ctaLabel`, `intervals[]`).</ParamField>

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

export default function PricingTable() {
  // productId derived from the active subscription / single-product org
  const { pricing, isLoading, error } = usePricing();

  if (isLoading) return <div>Loading…</div>;
  if (error) return <div>Couldn’t load pricing.</div>;
  if (!pricing) return <div>Select a product to see pricing.</div>;

  return (
    <table>
      <thead>
        <tr>
          <th>{pricing.product.name}</th>
          {pricing.tiers.map((tier) => (
            <th key={tier.id}>{tier.name}{tier.isPopular ? ' ★' : ''}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {pricing.features.map((feature) => (
          <tr key={feature.slug}>
            <td>{feature.name}</td>
            {pricing.tiers.map((tier) => {
              const ent = tier.entitlementBySlug[feature.slug];
              if (!ent) return <td key={tier.id}>—</td>;
              if (ent.mode === 'unlimited') return <td key={tier.id}>Unlimited</td>;
              return <td key={tier.id}>{ent.value ?? ent.periodAllocation}</td>;
            })}
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

## useBranding

Returns the customer-facing merchant identity for the current org — what the portal renders in its header instead of a hardcoded asset. Read-only, from `GET /v1/branding`, and cached aggressively since branding changes rarely. It's managed in the merchant dashboard.

**Parameters** — none.

**Returns**

* `branding` — the `Branding` object, or `null` while loading:

<ParamField body="name" type="string">Merchant display name. Always present.</ParamField>
<ParamField body="logo" type="string | null">Merchant logo URL, or `null` if none uploaded.</ParamField>
<ParamField body="website" type="string | null">Merchant website URL, or `null` if unset — powers the portal's "Return to {merchant}" back-link.</ParamField>
<ParamField body="branding" type="Record<string, unknown> | null">Typed appearance/theme config (accents, CSS overrides), or `null` if unset.</ParamField>

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

export default function PortalHeader() {
  const { branding, isLoading } = useBranding();
  if (isLoading || !branding) return null;

  return (
    <header>
      {branding.logo ? (
        <img src={branding.logo} alt={branding.name} height={28} />
      ) : (
        <span>{branding.name}</span>
      )}
      {branding.website ? (
        <a href={branding.website}>Return to {branding.name}</a>
      ) : null}
    </header>
  );
}
```

## See also

<CardGroup cols={2}>
  <Card title="Checkout" icon="cart-shopping" href="/react/checkout">
    Preview and start a subscription to a plan from the catalog.
  </Card>

  <Card title="Entitlements & gates" icon="lock" href="/react/entitlements-and-gates">
    What each plan's entitlements grant the customer at runtime.
  </Card>

  <Card title="Credits & wallets" icon="coins" href="/react/credits-and-wallets">
    The credit packages and wallets referenced by the pricing matrix.
  </Card>
</CardGroup>
