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

# Plan Versions and Migrations

> List a plan's versions, sell an older one, and move subscribers from one version to another at the end of their billing period.

Base URL: `https://api.nozle.app/core/api/v1`.

**Auth:** secret key only. A publishable key gets `403 secret_key_required`.

A change to what a plan bills or grants, made while customers pay for it, saves the plan's next version under the same code. Existing subscribers stay on the version they are on; new subscriptions get the current version. These endpoints read the versions and move subscribers between them. See [Plan versioning](https://docs.nozle.dev/guides/billing/plan-versions.md) for how versions are made and sold.

## List the versions of a plan

```http theme={null}
GET /plans/pro/versions
Authorization: Bearer sk_...
```

```json theme={null}
{
  "versions": [
    { "nozle_id": "1a90…", "version": 1, "is_current": false, "created_at": "2026-10-01T09:00:00Z", "amount_cents": 4900, "amount_currency": "USD", "active_subscriptions_count": 42 },
    { "nozle_id": "2b81…", "version": 2, "is_current": true, "created_at": "2026-10-15T09:00:00Z", "amount_cents": 5900, "amount_currency": "USD", "active_subscriptions_count": 7 }
  ]
}
```

Oldest first. Read one version in full with `GET /plans/pro?version=1`. Without `version`, or with a publishable key, `GET /plans/{code}` returns the current version.

## Subscribe to an older version

```http theme={null}
POST /subscriptions
```

```json theme={null}
{ "subscription": { "external_customer_id": "cus_123", "plan_code": "pro", "plan_version": 1, "external_id": "sub_cus_123" } }
```

`plan_version` is optional; without it the subscription gets the current version. The Engine's [`POST /subscribe`](https://docs.nozle.dev/api/subscribe) takes the same field.

| Response | When |
| - | - |
| `200` | The subscription, with `plan_version`. Sending the same `external_id` and `plan_code` again returns it unchanged, on its own version. |
| `422` `plan_version: plan_version_change_not_supported` | The subscription exists on another version. Use a migration. |
| `404` `plan_version_not_found` | The code has no such version. |

## Preview a migration

```http theme={null}
POST /plans/pro/version-migrations/preview
Content-Type: application/json
```

```json theme={null}
{ "from_version": 1 }
```

`to_version` is optional and defaults to the current version. Nothing is written.

```json theme={null}
{
  "plan_version_migration_preview": {
    "plan_code": "pro",
    "from_version": 1,
    "to_version": 2,
    "items": [
      { "external_subscription_id": "sub_1", "external_customer_id": "cus_1", "outcome": "move", "skip_reason": null, "effective_on": "2026-11-01", "current_amount_cents": 4900, "new_amount_cents": 5900 },
      { "external_subscription_id": "sub_2", "external_customer_id": "cus_2", "outcome": "skip", "skip_reason": "custom_pricing", "effective_on": null, "current_amount_cents": 3900, "new_amount_cents": 5900 }
    ]
  }
}
```

## Start a migration

```http theme={null}
POST /plans/pro/version-migrations
```

```json theme={null}
{ "from_version": 1 }
```

It returns `200` with `status: "running"`; the migration then works through the subscriptions in the background. Each subscription that can move is scheduled to move at the end of its current billing period, like a scheduled downgrade. **Nothing is invoiced, credited or refunded when it runs**; the subscription's next invoice bills the new version, and usage of the period that ended is billed at the old version's rates.

| Response | When |
| - | - |
| `404` `plan_not_found` | Unknown code or version. |
| `409` `plan_version_migration_running` | Another migration of this code is running. |
| `422` `to_version: same_version` | Both versions are the same. |
| `422` `to_version: plan_pending_deletion` | The target version is being deleted. |

## Follow, list and cancel

`GET /plans/pro/version-migrations/{id}` returns the migration with one item per subscription:

```json theme={null}
{
  "plan_version_migration": {
    "nozle_id": "7a51…",
    "plan_code": "pro",
    "from_version": 1,
    "to_version": 2,
    "status": "succeeded",
    "matched_count": 3,
    "succeeded_count": 2,
    "skipped_count": 1,
    "failed_count": 0,
    "started_at": "2026-10-22T10:00:00Z",
    "finished_at": "2026-10-22T10:00:04Z",
    "canceled_at": null,
    "created_at": "2026-10-22T10:00:00Z",
    "items": [
      { "external_subscription_id": "sub_1", "status": "succeeded", "skip_reason": null, "error": null, "effective_at": "2026-11-01T00:00:00Z", "move_status": "pending" },
      { "external_subscription_id": "sub_2", "status": "skipped", "skip_reason": "custom_pricing", "error": null, "effective_at": null, "move_status": null }
    ]
  }
}
```

| Field | Meaning |
| - | - |
| `status` | `running`, then `succeeded` once every subscription was handled (each move is then scheduled), `completed_with_errors` when some failed, `failed`, or `canceled`. |
| item `status` | `succeeded` (the move is scheduled or done), `skipped` (see `skip_reason`), `failed` (see `error`; a rerun retries it), `canceled`. |
| item `move_status` | For a scheduled move: `pending` until the move day, `moved` once the subscription is on the new version, `canceled` when the subscription ended first. |
| item `effective_at` | The day the subscription starts on the new version. |

`GET /plans/pro/version-migrations` lists the code's migrations, newest first, without items. `POST /plans/pro/version-migrations/{id}/cancel` stops the migration and unschedules every move that has not happened; those subscribers stay on their version.

The skip reasons are listed in [Plan versioning](https://docs.nozle.dev/guides/billing/plan-versions.md#move-subscribers-to-a-newer-version).

> [!NOTE]
>
> A migration never charges anyone when it runs, and a move never makes a credit note. Each subscriber pays the old version for the period they already paid for, then the new version from their next period.


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