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

# Plans & pricing

> A plan sets the price and billing interval. Customers subscribe to plans.

A plan defines how much to charge and how often. Plans live inside plan groups — a group is your tier structure (e.g., "Core Plans" with Starter, Pro, and Max inside it).

## Plan groups and plans

A **plan group** is a logical container. You might have one group called "Monthly Plans" and another called "Annual Plans", or one called "Core" and another called "Enterprise".

A **plan** belongs to a group and has:

* A name (e.g., "Pro")
* An amount in kobo (e.g., 500000 for ₦5,000)
* A billing interval (`month` or `year`)
* An optional trial period in days
* A required **`lookup_key`** — a stable handle (e.g. `pro_monthly`) you reference instead of the UUID

## Create a plan group and plan

First, create the group:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.useplinth.com/v1/plan-groups \
    -H "Authorization: Bearer sk_test_DOCS_SHARED_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Core Plans",
      "description": "Standard monthly tiers"
    }'
  ```

  ```typescript Node.js theme={null}
  const group = await client.planGroups.create({
    name: 'Core Plans',
    description: 'Standard monthly tiers',
  });
  // → { id: 'pg_01JABC...', name: 'Core Plans', ... }
  ```
</CodeGroup>

Then create plans inside it:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.useplinth.com/v1/plan-groups/pg_01JABC.../plans \
    -H "Authorization: Bearer sk_test_DOCS_SHARED_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Pro",
      "amount_minor": 500000,
      "interval": "month",
      "trial_period_days": 14,
      "lookup_key": "pro_monthly"
    }'
  ```

  ```typescript Node.js theme={null}
  const plan = await client.planGroups.createPlan('pg_01JABC...', {
    name: 'Pro',
    amountMinor: 500000,
    interval: 'month',
    trialPeriodDays: 14,
    lookupKey: 'pro_monthly',
  });
  ```
</CodeGroup>

```json Response theme={null}
{
  "id": "plan_pro_01...",
  "plan_group_id": "pg_01JABC...",
  "name": "Pro",
  "amount_minor": 500000,
  "interval": "month",
  "trial_period_days": 14,
  "active": true,
  "created_at": "2026-06-01T00:00:00Z"
}
```

## Trial periods

Set `trial_period_days` on a plan. New subscribers start in `trialing` state and aren't charged until the trial ends. When the trial expires, the subscription transitions to `active` and the first invoice is created.

If a subscriber cancels during trial, they're never charged. The `subscription.canceled` webhook fires.

## Amounts are in kobo

<Note>
  All `amount_minor` values are in kobo — 1/100th of a Naira. Never pass decimal values.
</Note>

| Naira price   | `amount_minor` |
| ------------- | -------------- |
| ₦500/month    | 50000          |
| ₦2,000/month  | 200000         |
| ₦5,000/month  | 500000         |
| ₦12,000/month | 1200000        |
| ₦120,000/year | 12000000       |

## Lookup keys

Every plan **requires** a `lookup_key` (lowercase letters, numbers, underscores — unique per tenant) so your app references a plan by a **stable role** instead of an environment-specific UUID:

```bash theme={null}
curl -X POST https://api.useplinth.com/v1/plans \
  -H "Authorization: Bearer sk_test_DOCS_SHARED_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "plan_group_id": "pg_01JABC...", "name": "Pro", "amount_minor": 500000,
        "billing_interval": "month", "lookup_key": "pro_monthly" }'
```

Now your app can `GET /v1/plans`, find the plan whose `lookup_key` is `pro_monthly`, and use its id — no hardcoded ids, and the mapping survives re-provisioning, renames, and price changes. (Reusing a key returns `409`.)

## Changing prices, archiving & deleting plans

<Warning>
  **Prices are immutable once a plan has subscribers.** Changing `amount_minor` or the interval would silently re-price everyone at their next renewal, so Plinth rejects it (`400 plan_immutable`). Marketing fields (name, `lookup_key`, trial days) can always change.
</Warning>

To **change a price**: create a *new* plan (new price, new `lookup_key` e.g. `pro_monthly_v2`), archive the old one, then either grandfather existing subscribers or migrate them with a [plan change](/api-reference/change-plan).

**Deleting is archive-by-default.** `DELETE /v1/plans/{id}`:

* If the plan **has subscribers** → it's **archived** (`active: false`): existing subscribers keep billing, history is preserved, and it stops accepting new sign-ups. Response: `{ archived: true, deleted: false }`.
* If the plan was **never subscribed** → it's **hard-deleted**. Response: `{ archived: false, deleted: true }`.

Historical invoices are never affected either way — they snapshot the amount at billing time.

## List all plans

```bash theme={null}
curl https://api.useplinth.com/v1/plans \
  -H "Authorization: Bearer sk_test_DOCS_SHARED_KEY"
```

Returns **active plans only** by default. Pass `?include_archived=true` to include archived plans.
