> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Plans

> Plans are pricing composed from options. Recurring, usage-based, hybrid, and one-time are derived labels, not stored types.

A plan is pricing you attach to a product. It is a single type with an ordered `options[]` list. What a plan “is” — recurring, usage-based, hybrid, or one-time — is a **derived label**, not a field you send.

Console templates (Recurring, Usage-based, One-time, Hybrid, Blank) only seed that list. They are not persisted types.

Money on options is integer **minor units** of the option’s currency (cents for USD). Usage rates in the console are money per meter noun (`$0.02 / request`). Credits are only the prepaid wallet — see [Plans and billing glossary](/plans/glossary).

## How a plan is stored

```
Product (prd_myapi)
├── Plan: Free (pln_abc123) — billingCycle + $0 flat charge + autoAssigned
├── Plan: Pro (pln_def456) — billingCycle + $19 flat charge
└── Plan: Usage (pln_ghi789) — per-request charge + included limit
```

Plans live on the product. There is no separate plans collection, no plan `type` discriminator, and no subtype fields such as `setupFee`, `creditsPerUnit`, `basePrice`, or `freeUnits`.

| You send                        | The API derives                                                        |
| ------------------------------- | ---------------------------------------------------------------------- |
| `options[]`                     | `planType` / `type` (`recurring`, `usage-based`, `hybrid`, `one-time`) |
| Positive charge or tier amounts | `requiresPayment`                                                      |
| `kind: "hidden"`                | Hidden from customer surfaces                                          |
| `kind: "autoAssigned"`          | Product fallback / pre-selected plan                                   |

`planType` on some responses is a back-compat label for analytics and UI. Do not send `type` when you create or update a plan.

## Option catalog

Each option is discriminated on `kind`. Optional `label` is allowed on every kind.

### `charge`

A price. `per` is `flat`, `unit`, or `seat`. Per-unit charges require `meter`.

| Field         | Type      | Description                               |
| ------------- | --------- | ----------------------------------------- |
| `amountMinor` | `number`  | Amount in `currency`, integer minor units |
| `currency`    | `string`  | ISO 4217 code                             |
| `per`         | `enum`    | `flat`, `unit`, or `seat`                 |
| `meter`       | `string`  | Required when `per` is `unit`             |
| `oneTime`     | `boolean` | Setup fee. Only valid on `per: "flat"`    |

A plan offers several currencies by holding multiple charges of the same role that differ only by `currency`. Amounts are set per currency, not FX-converted.

### `billingCycle`

Makes the plan recurring. Omit it for one-time or pay-as-you-go usage.

| Field      | Type     | Description                                                               |
| ---------- | -------- | ------------------------------------------------------------------------- |
| `interval` | `enum`   | `week`, `month`, or `year`                                                |
| `count`    | `number` | Every N intervals. Defaults to `1`. Quarterly is `month` + `count: 3`     |
| `anchor`   | `string` | Optional first cycle boundary (ISO datetime). Defaults to activation time |

### `tier`

Volume or graduated rates for a meter. The embedded `charge` is per-unit. One
option per band — see [Tiered](#tiered-banded-rates-for-one-meter) for a worked
example and for how the two modes differ.

| Field    | Type             | Description                                                                                                       |
| -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
| `from`   | `number`         | Start of the band, inclusive. The first band starts at `0`                                                        |
| `to`     | `number \| null` | End of the band, exclusive — the next band starts here. `null` is the unbounded top band                          |
| `mode`   | `enum`           | `graduated` or `volume`. Every band on a meter must use the same one                                              |
| `charge` | `object`         | Embedded per-unit charge (`amountMinor`, `currency`, `per: "unit"`, `meter`). Its `meter` is what groups the band |

### `limit`

Included cap for a meter. Copy says **included**. `cap: 0` means unlimited.

| Field               | Type     | Description                                                          |
| ------------------- | -------- | -------------------------------------------------------------------- |
| `cap`               | `number` | Included count. `0` is unlimited                                     |
| `scope`             | `enum`   | `billing_period`, `lifetime`, or `rolling_window`                    |
| `meter`             | `string` | Meter this cap applies to                                            |
| `onExceed`          | `enum`   | `block`, `throttle`, `charge`, `notify`, `top_up`, or `auto_upgrade` |
| `windowDays`        | `number` | Required when `scope` is `rolling_window`                            |
| `onExceedPricingId` | `string` | Target plan when `onExceed` is `auto_upgrade`                        |

A missing limit is pay-as-you-go (every counted item is billed). It is not unlimited.

### `entitlement`

A feature flag or numeric benefit on the plan. The console label is **Benefit**. The wire kind stays `entitlement`.

| Field     | Type                          | Description    |
| --------- | ----------------------------- | -------------- |
| `feature` | `string`                      | Feature key    |
| `value`   | `boolean \| number \| string` | Granted value  |
| `scope`   | `string`                      | Optional scope |

### `trial`

| Field                  | Type      | Description                             |
| ---------------------- | --------- | --------------------------------------- |
| `days`                 | `number`  | Trial length                            |
| `onEnd`                | `enum`    | `convert`, `cancel`, or `downgrade`     |
| `requireCard`          | `boolean` | Collect a card before the trial starts  |
| `downgradeToPricingId` | `string`  | Target plan when `onEnd` is `downgrade` |

### `discount`

| Field       | Type     | Description                                                 |
| ----------- | -------- | ----------------------------------------------------------- |
| `mode`      | `enum`   | `percentage` or `fixed`                                     |
| `value`     | `number` | Percentage: basis points (`5000` = 50%). Fixed: minor units |
| `currency`  | `string` | Required when `mode` is `fixed`                             |
| `duration`  | `enum`   | `once`, `repeating`, or `forever`                           |
| `cycles`    | `number` | Required when `duration` is `repeating`                     |
| `appliesTo` | `enum`   | `recurring`, `setup`, `usage`, or `all`                     |

### `rollover`

What happens to unused included usage at period end.

| Field       | Type     | Description                                                   |
| ----------- | -------- | ------------------------------------------------------------- |
| `meter`     | `string` | Meter to roll                                                 |
| `treatment` | `enum`   | `forfeit`, `carry_forward`, `credit_units`, or `credit_money` |
| `capUnits`  | `number` | Optional carry-forward cap                                    |
| `maxCycles` | `number` | Optional max periods unused usage can carry                   |
| `rateMinor` | `number` | Per-item value when `treatment` is `credit_money`             |
| `currency`  | `string` | Required when `treatment` is `credit_money`                   |

### Markers

| Kind           | What it does                                                       |
| -------------- | ------------------------------------------------------------------ |
| `autoAssigned` | Product fallback / pre-selected plan. At most one per product      |
| `hidden`       | Off every customer-facing surface. Grant only by direct assignment |

Markers are catalog metadata. They do not change pricing math.

## Common compositions

These labels are derived. Send the options, not `"type": "…"`.

### Recurring — flat charge + billing cycle

```json theme={null}
{
  "name": "Pro",
  "currency": "USD",
  "options": [
    { "kind": "billingCycle", "interval": "month" },
    { "kind": "charge", "per": "flat", "amountMinor": 1900, "currency": "USD" },
    { "kind": "trial", "days": 14, "onEnd": "convert" }
  ]
}
```

A \$0 flat charge plus `autoAssigned` is a free recurring fallback.

### Usage-based — per-unit charge, no billing cycle

```json theme={null}
{
  "name": "Pay as you go",
  "currency": "USD",
  "options": [
    {
      "kind": "charge",
      "per": "unit",
      "amountMinor": 2,
      "currency": "USD",
      "meter": "requests"
    },
    {
      "kind": "limit",
      "cap": 1000,
      "scope": "billing_period",
      "meter": "requests",
      "onExceed": "charge"
    }
  ]
}
```

`amountMinor: 2` is \$0.02 per request. Do not send `creditsPerUnit` as a plan field.

Minimum top-up is a provider setting (`minTopUpMinor` in SolvaPay Console settings), not a plan option. See [Auto-recharge](/sdks/typescript/guides/auto-recharge).

### Tiered — banded rates for one meter

A `tier` option is one band. Send one per band, contiguous and covering the whole
range: each band's `from` is the previous band's `to`, the first starts at `0`,
and exactly one band — the last — sets `to: null` for everything above it.

```json theme={null}
{
  "name": "Scale",
  "currency": "USD",
  "options": [
    {
      "kind": "tier",
      "from": 0,
      "to": 1000,
      "mode": "graduated",
      "charge": { "per": "unit", "amountMinor": 2, "currency": "USD", "meter": "requests" }
    },
    {
      "kind": "tier",
      "from": 1000,
      "to": null,
      "mode": "graduated",
      "charge": { "per": "unit", "amountMinor": 1, "currency": "USD", "meter": "requests" }
    }
  ]
}
```

Every band in a group shares one `mode`, and the two modes price the same usage
differently. For 1,500 requests against the plan above:

| `mode`      | What it charges                                                                | Total   |
| ----------- | ------------------------------------------------------------------------------ | ------- |
| `graduated` | Each band prices the requests that fall inside it: 1,000 × $0.02 + 500 × $0.01 | \$25.00 |
| `volume`    | The total picks one band and prices everything at that rate: 1,500 × \$0.01    | \$15.00 |

Group bands by meter. A plan can tier two meters by sending two groups, each
contiguous in its own `charge.meter`. Bands are grouped by that meter, never by
their position in `options[]`.

A meter is priced by bands **or** by a `charge` with `per: "unit"` — not both.
Both are per-request prices on the same requests, so sending both is rejected.

Bands and `limit` compose: the `cap` is the included count, and the bands price
the overage above it. A plan with `"cap": 1000` and the bands above includes the
first 1,000 requests and prices request 1,001 onward from the band it lands in.
Set `onExceed: "charge"` so exceeding the included count bills rather than blocks.

Tiered usage on a recurring plan bills at the end of each billing period. On a
plan with no billing cycle it is charged in real time against the credit balance.

### Hybrid — recurring fee plus metered usage

```json theme={null}
{
  "name": "Pro plus usage",
  "currency": "USD",
  "options": [
    { "kind": "billingCycle", "interval": "month" },
    { "kind": "charge", "per": "flat", "amountMinor": 4900, "currency": "USD" },
    {
      "kind": "charge",
      "per": "unit",
      "amountMinor": 2,
      "currency": "USD",
      "meter": "requests"
    },
    {
      "kind": "limit",
      "cap": 1000,
      "scope": "billing_period",
      "meter": "requests",
      "onExceed": "charge"
    }
  ]
}
```

Bands replace the flat per-unit charge rather than joining it — a meter is priced one way or the other. The derived label is still `hybrid` when a billing cycle and a meter are both present.

### One-time — flat charge, no cycle

```json theme={null}
{
  "name": "Lifetime",
  "currency": "USD",
  "options": [
    { "kind": "charge", "per": "flat", "amountMinor": 9900, "currency": "USD" }
  ]
}
```

## Identity fields

These sit on the plan next to `options[]`:

| Field              | Type     | Description                                            |
| ------------------ | -------- | ------------------------------------------------------ |
| `reference`        | `string` | Auto-generated (`pln_XXXXXXXX`)                        |
| `name`             | `string` | Plan name                                              |
| `description`      | `string` | Optional                                               |
| `currency`         | `string` | Display currency. Defaults from the first money option |
| `status`           | `enum`   | `active`, `inactive`, or `archived`                    |
| `features`         | `object` | Optional feature flags                                 |
| `metadata`         | `object` | Optional provider metadata                             |
| `maxActiveUsers`   | `number` | Optional concurrent-user cap                           |
| `accessExpiryDays` | `number` | Optional access lifetime                               |
| `taxBehavior`      | `enum`   | `auto`, `inclusive`, or `exclusive`                    |

## Hidden and auto-assigned plans

Add `{ "kind": "hidden" }` to keep a plan off checkout, the SDK catalog, and MCP. Customers cannot self-select it. Assign it from the SolvaPay Console customer page. Direct assignment stamps the purchase with `admin_assignment`.

Add `{ "kind": "autoAssigned" }` to make the plan the product fallback (MCP tool access when no tool-specific plan is set, and new-customer pre-selection). Setting a new auto-assigned plan clears the marker on the previous one.

## Plans are embedded in products

A product owns its plans. Create, update, and delete them through the product. Plan references are globally unique.

## Plan API

All plan endpoints are scoped to a product. Send `options[]`. Do not send `type`, `creditsPerUnit`, `basePrice`, `setupFee`, or `freeUnits`.

| Method   | Endpoint                                   | Description              |
| -------- | ------------------------------------------ | ------------------------ |
| `GET`    | `/sdk/products/:productRef/plans`          | List plans for a product |
| `GET`    | `/sdk/products/:productRef/plans/:planRef` | Get a single plan        |
| `POST`   | `/sdk/products/:productRef/plans`          | Create a plan            |
| `PUT`    | `/sdk/products/:productRef/plans/:planRef` | Update a plan            |
| `DELETE` | `/sdk/products/:productRef/plans/:planRef` | Delete a plan            |

`PUT` replaces the full `options[]` when you send it.

## Plans and MCP servers

* **Server default plan** — `defaultPlanRef` on an MCP server uses the product’s auto-assigned plan when no tool-specific plan is set
* **Per-tool plan assignments** — tools can reference plans via `planIds`

When plans change, MCP server configurations sync automatically.

## Purchase snapshots

A purchase freezes the plan’s `options[]` at buy time. Changing the live plan does not change existing purchases.

```json theme={null}
{
  "planSnapshot": {
    "reference": "pln_abc123",
    "name": "Pro",
    "price": 1900,
    "currency": "USD",
    "options": [
      { "kind": "billingCycle", "interval": "month" },
      { "kind": "charge", "per": "flat", "amountMinor": 1900, "currency": "USD" }
    ]
  }
}
```

`price` and `planType` on the snapshot are derived from those options.

## Next steps

* [Billing](/plans/billing) — cycles, usage, trials, and renewals
* [Plans and billing glossary](/plans/glossary) — credits, meter noun, included vs wallet remaining
* [Usage events](/sdks/typescript/guides/usage-events) — record usage from the TypeScript SDK
