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

# Plans

> The plans customers can switch between on their billing page: a name, features, and a monthly or yearly product.

A plan is what a customer sees on their billing page: a name, a short list of
features, and the [product](/dev/api-reference/products) it bills monthly,
yearly, or both. Prices are read live from those products, so changing a
product's price changes every plan that uses it.

A plan holds no money state. It decides what the page **offers**; Stripe still
holds what each customer pays.

| Endpoint | Scope | Does |
| - | - | - |
| `GET /v1/plans` | `billing:read` | Lists plans in the order customers see them |
| `POST /v1/plans` | `billing:admin` | Creates a plan |
| `PATCH /v1/plans/{key}` | `billing:admin` | Changes the named fields |
| `DELETE /v1/plans/{key}` | `billing:admin` | Removes a plan from the page |
| `POST /v1/plans/order` | `billing:admin` | Sets the order |

A plan is named by its **key**: lowercase letters, digits and dashes, up to 40
characters (`growth`, `pro-annual`). A workspace holds up to 12 plans.

## The Plan object

<ResponseExample>
  ```json A plan theme={null}
  {
    "key": "growth",
    "name": "Growth",
    "description": "For teams past their first ten customers",
    "features": ["Unlimited seats", "Priority support"],
    "highlighted": true,
    "ctaLabel": "",
    "trialDays": 14,
    "monthly": {
      "product": "rec_…",
      "productName": "Growth monthly",
      "price": 99,
      "unitAmount": 9900,
      "currency": "usd",
      "interval": "month",
      "stripe": "synced"
    },
    "yearly": null,
    "contactSalesUrl": "",
    "position": 1
  }
  ```
</ResponseExample>

`monthly` and `yearly` carry the product's live price and its [Stripe
state](/dev/api-reference/products#the-product-object). A plan whose product
reads `out_of_sync` would charge a different amount than it shows; push the price
from **Revenue › Billing page** or by saving the product again.

## Create a plan

`POST /v1/plans`

```ts theme={null}
await custral.plans.create({
  key: "growth",
  name: "Growth",
  features: ["Unlimited seats", "Priority support"],
  monthlyProduct: "Growth monthly",
  yearlyProduct: "Growth yearly",
  highlighted: true,
  trialDays: 14,
});
```

<ParamField body="key" type="string" required>
  Unique in the workspace.
</ParamField>

<ParamField body="name" type="string" required>
  Up to 80 characters.
</ParamField>

<ParamField body="description" type="string">
  Up to 280 characters.
</ParamField>

<ParamField body="features" type="string[]">
  Up to 12 lines, each up to 120 characters.
</ParamField>

<ParamField body="highlighted" type="boolean">
  Marks the plan as the one to pick.
</ParamField>

<ParamField body="ctaLabel" type="string">
  The button's words. `""` uses "Switch to" and the plan's name.
</ParamField>

<ParamField body="trialDays" type="integer">
  0 to 90. Applies when a customer with no subscription chooses this plan.
</ParamField>

<ParamField body="monthlyProduct" type="string">
  The product billed monthly, by id or exact name. It must be a monthly,
  unmetered product. `""` clears it.
</ParamField>

<ParamField body="yearlyProduct" type="string">
  The product billed yearly, the same way.
</ParamField>

<ParamField body="contactSalesUrl" type="string">
  An `https` link shown instead of a switch button. `""` clears it.
</ParamField>

A plan needs at least one of `monthlyProduct`, `yearlyProduct` or
`contactSalesUrl`.

## Change, remove and order plans

`PATCH /v1/plans/{key}` takes the same fields, all optional; omitted fields stay
as they are. `DELETE /v1/plans/{key}` removes the plan from the page and returns
`{deleted: true}`. Customers already on it keep their Stripe subscription.

`POST /v1/plans/order` takes every plan's key, once, in the order customers see
them:

```ts theme={null}
await custral.plans.reorder({keys: ["starter", "growth", "scale"]});
```

## Errors

| Code | When |
| - | - |
| `plan_key_taken` | Another plan has that key. |
| `plan_limit_reached` | The workspace already has 12 plans. |
| `plan_not_found` | No plan has that key. |
| `plan_name_required` | `name` is empty. |
| `plan_needs_price` | No monthly product, yearly product or sales link. |
| `product_not_found` | No product has the id or name you gave. |
| `product_ambiguous` | More than one product has that name. Use its id. |
| `plan_product_not_found` | The product was archived after it was named. |
| `plan_product_wrong_interval` | A monthly product billed yearly, or the other way round. |
| `plan_product_metered` | A metered product can't be a plan's price. |
| `plan_contact_url_invalid` | `contactSalesUrl` is not `https`. |
| `plan_order_mismatch` | `keys` doesn't name every plan exactly once. |

See [Errors](/dev/errors/overview) for the HTTP status each code maps to.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.