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

# Subscription Plans

> Attach a recurring price and cadence to your products, so a subscription checkout needs only a product and a quantity.

## What a subscription plan is

A **subscription plan** is a recurring price and billing cadence: a price in cents, an interval — exactly one of `day`, `week`, `month`, `year` — and how many intervals make up one cycle. (The older adverb words `daily`, `weekly`, `monthly`, `quarterly`, `yearly` are not accepted; the v1 adverb words must be converted before calling /v2/plans.)

A plan doesn't have to belong to any product — you can create one on its own — but a subscription checkout needs a product that has an active plan **attached**. The same plan can be attached to several products at once; nothing about the plan changes per product.

Every subscription line item at checkout carries a `plan_id`. Gale reads the price and cadence from the plan itself, so a cart never repeats them:

```js theme={null}
metadata: { payment_source: 'stripe_elements_cpm', mode: 'subscription' },
line_items: [{ product_id: 'SKU-123', quantity: 1, plan_id: 'plan_01K9…' }]
```

Sending `interval` or `interval_count` in the cart metadata on a subscription cart is rejected — cadence comes from the plan only, never from the call.

Subscription plans work whichever way you use Gale — as a custom payment method inside Stripe Elements, or as your payment gateway directly. They live in Gale: a plan is not read from or synced with any other system, so what you set here is exactly what bills.

## Creating and managing subscription plans

| Method   | Endpoint                                 | Purpose                                                                                                                |
| -------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `GET`    | `/v2/plans`                              | List your plans — paginated (`per_page`, `page`). Filter with `?status=active` / `?status=archived` and `?product_id=` |
| `POST`   | `/v2/plans`                              | Create a plan                                                                                                          |
| `GET`    | `/v2/plans/{id}`                         | One plan                                                                                                               |
| `PATCH`  | `/v2/plans/{id}`                         | Change a plan — future cycles only                                                                                     |
| `DELETE` | `/v2/plans/{id}`                         | Archive a plan — returns **200** with the archived plan, not an empty response                                         |
| `POST`   | `/v2/products/{product_id}/attach-plans` | Attach one or more existing plans to a product                                                                         |
| `POST`   | `/v2/products/{product_id}/detach-plans` | Detach one or more plans from a product                                                                                |

```js theme={null}
await fetch(`${GALE_API_URL}/api/v2/plans`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${GALE_MERCHANT_API_KEY}`,   // server-side only
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Monthly Lens Supply',
    amount: 2599,          // PER UNIT — a cycle bills amount × quantity
    currency: 'USD',            // optional, defaults to USD
    interval: 'month',
    interval_count: 1,          // optional, 1–365, default 1
    trial_days: 0,              // must be 0 — see the trials note below
    product_id: 'SKU-123',      // optional — a plan can exist unattached
  }),
});
```

**Success 201**

```json theme={null}
{
  "plan_id": "plan_01K9XYPLAN000000000000000",
  "name": "Monthly Lens Supply",
  "amount": 2599,
  "currency": "USD",
  "interval": "month",
  "interval_count": 1,
  "trial_days": 0,
  "status": "active",
  "products": [
    { "product_id": "SKU-123", "name": "Daily Vitamin" }
  ],
  "source": "gale",
  "created_at": "2026-08-20T12:00:00Z"
}
```

`name` and `amount` are required. `amount` is the only accepted spelling — there is no `amount` alias; sending `amount` instead of `amount` is rejected. `interval` must be exactly one of `day`, `week`, `month`, `year`. `currency` accepts only uppercase `USD` — `usd` is rejected.

`currency` can only be set on create — it's rejected on an update. `status` can only be set on an update, not on create.

`source` is `"gale"` for a plan created through this API; it would carry an external provider's name instead for a plan that originated elsewhere.

**Attaching one plan to several products** — call `attach-plans` once per product with the same `plan_id`:

```js theme={null}
await fetch(`${GALE_API_URL}/api/v2/products/SKU-123/attach-plans`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${GALE_MERCHANT_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ plan_ids: ['plan_01K9XYPLAN000000000000000'] }),
});
// { "product_id": "SKU-123", "attached_plan_ids": ["plan_01K9XYPLAN000000000000000"] }

await fetch(`${GALE_API_URL}/api/v2/products/SKU-456/attach-plans`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${GALE_MERCHANT_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ plan_ids: ['plan_01K9XYPLAN000000000000000'] }),
});
// { "product_id": "SKU-456", "attached_plan_ids": ["plan_01K9XYPLAN000000000000000"] }
```

`GET /v2/plans/plan_01K9XYPLAN000000000000000` now returns both products in `products[]`. `detach-plans` takes the same body shape and removes the pairing without touching the plan itself.

**The list is paginated and its filters are strict.** `GET /v2/plans` takes `per_page` (default 50, max 200) and `page`, and reports `metadata.pagination` with `current_page`, `per_page`, `total` and `total_pages` — `data` stays a flat array. A `status` that isn't `active` or `archived` is a `422`, and a `product_id` that isn't a product on your account is a `404 PRODUCT_NOT_FOUND` — neither comes back as an empty list, so "no results" always means no results.

**Editing a plan affects future cycles only.** A cycle that has already been charged keeps the amount it was charged at, and there's no proration. **Archiving a plan** stops it being used for new subscriptions; subscriptions already running on it keep billing.

## Trials aren't honoured yet

`trial_days` must be `0` or omitted. Sending a non-zero value is rejected with a **`422` at plan creation** — the billing engine doesn't act on `trial_days` yet, so a plan carrying a trial could never be checked out anyway, and refusing it up front is clearer than accepting it and failing later at checkout.

## What changes at checkout

Send `mode: 'subscription'` in the cart metadata; every subscription line item carries `product_id`, `quantity`, and **`plan_id`** — it's required, not optional. Gale fills in the rest from its own records:

* **`interval` and `interval_count`** come from the plan. Sending either in the cart metadata is rejected.
* **`price`** comes from the plan, multiplied by `quantity`. If you do send a price it must match what that works out to, otherwise the call is rejected rather than silently billing a different amount than your checkout page displayed.
* **`name` and `currency`** come from the product and its plan.

`422 PLAN_NOT_FOUND` covers every way a plan reference can fail to resolve: the product has no attached active plan, the `plan_id` doesn't exist, it's archived, it isn't attached to that product, or it belongs to another merchant. Gale returns the same code for all of these on purpose, rather than leaking which case it was.

## Related resources

<CardGroup cols="2">
  <Card title="Elements + PaymentIntent" icon="code" href="/custom-payment-methods/elements-payment-intent">
    Your backend hands off to Gale; Stripe is never in the payment path.
  </Card>

  <Card title="Elements + Checkout Session" icon="plug" href="/custom-payment-methods/elements-checkout-session">
    Stripe orchestrates the charge and owns the subscription object.
  </Card>

  <Card title="Subscription Webhooks" icon="bell" href="/webhooks/subscriptions">
    Renewals, failed payments, cancellations and disputes, as they happen.
  </Card>

  <Card title="Custom Payment Methods" icon="map" href="/custom-payment-methods/overview">
    Which of the two Stripe integration paths fits.
  </Card>
</CardGroup>
