> ## 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 Transition Policies

> Define how a subscription moves from one plan to another: direction, timing, billing anchor, proration, and settlement.

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

**Auth:** secret key only. When your organization has API key permissions, reads need `plan_transition_policy:read` and changes need `plan_transition_policy:write`. Keys that existed before this permission have the same access to it that they have to `plan`.

A plan transition policy tells Nozle how to move a subscription from one plan (the source) to another (the target). Policies are stored per pair of plans: `starter` to `growth` and `growth` to `starter` are two separate policies. Nozle does not infer the direction from the prices; you set it.

Changing an existing subscription's plan needs a policy for that exact pair. Without one, these calls return `422 plan_transition_policy_required`:

* [Checkout](/api/create-checkout) for a customer that already has an active subscription, including Entity subscription checkout and change-plan
* [Subscription change preview](/api/subscription-management) (`POST /subscriptions/preview`)
* [Settlement transitions](/api/subscription-management) of type `downgrade`, preview and apply

Withdrawing a pending change does not need a policy. Subscription options leave out every target plan that has no policy, so an empty `eligible_plans` list usually means no policy is configured.

A subscription on an override plan (a plan made from a catalog plan for one customer, for example a negotiated price or a subscription created from a quote) uses the policies of the catalog plan it was made from. Its options list the same eligible plans as a subscription on the catalog plan, and preview, checkout and transitions use the catalog plan's policy. The change replaces the override plan with the target catalog plan, so the negotiated price and other overrides do not carry over. A self-serve change from an override plan to its own catalog plan returns `422 plan_already_active`; a merchant plan change through `POST /subscriptions` can still replace it with the catalog plan.

> [!NOTE]
>
> One fallback exists. When the current plan is free and the target plan is paid, Nozle uses the organization's free-to-paid policy (Settings, General, "Checkout & billing policy" in the dashboard) as an upgrade. Every other pair, including paid to paid, paid to free, and free to free, needs its own policy. The dashboard sets only the free-to-paid default; create pair policies with this API.

## Create or update a policy

```http theme={null}
PUT /plans/starter/transition-policies/growth
Authorization: Bearer sk_...
Content-Type: application/json
```

```json theme={null}
{
  "plan_transition_policy": {
    "direction": "upgrade",
    "timing": "immediate",
    "billing_anchor": "keep_anchor",
    "proration_behavior": "prorate_immediately",
    "credit_action": "credit"
  }
}
```

`PUT` creates the policy for the pair, or updates the existing one. It returns `200` in both cases. On a new policy, fields you leave out take their defaults. On an existing policy, fields you leave out keep their current values. To return a field to its default, send the default value. `PATCH` on the same path does the same thing.

| Field | Values | Default | Meaning |
| - | - | - | - |
| `direction` | `upgrade`, `downgrade` | required | How Nozle treats the change. An immediate upgrade is payment-gated; an immediate downgrade is applied at once. |
| `timing` | `immediate`, `end_of_period` | `immediate` | When the change takes effect. |
| `billing_anchor` | `keep_anchor`, `reset_anchor` | `keep_anchor` | Keep the current renewal day, or start a new period on the change date. |
| `proration_behavior` | `prorate_immediately`, `none` | `prorate_immediately` | Whether the unused part of the current period is prorated. |
| `credit_action` | `credit`, `refund`, `offset`, `none` | `none` | What happens to the unused amount of the current plan. |
| `refund_mode` | `prorated`, `full` | `prorated` | How much is refunded when `credit_action` is `refund`. |
| `final_invoice_action` | `generate`, `skip` | `generate` | Whether a final invoice is created for the old plan. |
| `policy` | `prorate_keep_anchor`, `full_term_reset_anchor`, `next_period` | derived | A summary of the fields above. Nozle derives it: `next_period` for `end_of_period` timing, `full_term_reset_anchor` for `reset_anchor`, otherwise `prorate_keep_anchor`. You can send only `policy` to apply its preset. |

Validation rules:

* `end_of_period` timing needs `proration_behavior: "none"` and `credit_action: "none"`. Because `proration_behavior` defaults to `prorate_immediately`, send `"proration_behavior": "none"` with every `end_of_period` policy.
* `reset_anchor` needs `immediate` timing (`reset_requires_immediate_timing`).
* `refund_mode: "full"` needs `credit_action: "refund"` (`requires_refund_action`).
* `credit_action: "offset"` needs `final_invoice_action: "generate"` (`must_generate_for_offset`).
* Source and target must differ (`must_be_different`).

A violation returns `422` with `code: "validation_errors"` and the failing field in `error_details`:

```json theme={null}
{
  "status": 422,
  "error": "Unprocessable Entity",
  "code": "validation_errors",
  "error_details": { "billing_anchor": ["reset_requires_immediate_timing"] }
}
```

> [!WARNING]
>
> The two `end_of_period` rules currently return a long untranslated message in `error_details.proration_behavior` and `error_details.credit_action` instead of a short code. Check which field is present rather than matching the text.

An unknown target plan returns `404 plan_not_found`. An invalid enum value returns `422` with `value_is_invalid` for that field.

### Response

```json theme={null}
{
  "plan_transition_policy": {
    "nozle_id": "0d6a4e1c-2b7f-4f0e-9b57-7c2a5d1e8f33",
    "source_plan_code": "starter",
    "target_plan_code": "growth",
    "direction": "upgrade",
    "policy": "prorate_keep_anchor",
    "timing": "immediate",
    "billing_anchor": "keep_anchor",
    "proration_behavior": "prorate_immediately",
    "credit_action": "credit",
    "refund_mode": "prorated",
    "final_invoice_action": "generate",
    "created_at": "2026-10-12T18:24:51Z",
    "updated_at": "2026-10-12T18:24:51Z"
  }
}
```

## Read, list, and delete

| Request | Result |
| - | - |
| `GET /plans/{plan_code}/transition-policies/{target_plan_code}` | `200` with the policy, or `404 plan_transition_policy_not_found` when the pair has none. |
| `GET /plans/{plan_code}/transition-policies` | `200` with `{"plan_transition_policies": [...]}`: every policy whose source is this plan. |
| `DELETE /plans/{plan_code}/transition-policies/{target_plan_code}` | `200` with the deleted policy. |

## Common setups

Upgrade now, keep the renewal day, credit the unused time:

```json theme={null}
{ "plan_transition_policy": { "direction": "upgrade", "timing": "immediate", "credit_action": "credit" } }
```

Downgrade at the end of the period. Subscription options list a downgrade only when it uses `end_of_period` timing and `keep_anchor`:

```json theme={null}
{
  "plan_transition_policy": {
    "direction": "downgrade",
    "timing": "end_of_period",
    "billing_anchor": "keep_anchor",
    "proration_behavior": "none",
    "credit_action": "none"
  }
}
```

## Proration on a mid-cycle upgrade

When a subscription moves to another plan in the middle of its billing period, the policy for that pair decides what is charged:

* `timing: "immediate"` applies the change now. With `end_of_period`, the change waits for the next period and nothing is prorated (`proration_behavior` and `credit_action` must be `none`).
* `proration_behavior: "prorate_immediately"`, the default, prorates the unused part of the current period. `none` does not prorate it.
* `credit_action` decides what happens to that unused amount of the current plan: `credit` credits it, `refund` refunds it (`refund_mode` `prorated` refunds the unused part, `full` the whole amount), `offset` needs `final_invoice_action: "generate"`, and `none`, the default, does nothing with it.
* `billing_anchor: "keep_anchor"` keeps the current renewal day. `reset_anchor` starts a new period on the change date.
* An immediate upgrade with `proration_behavior: "none"` and `billing_anchor: "keep_anchor"` still bills the current plan for its period up to the change, as a cancellation does: its usage in arrears and, for a plan paid in arrears, its base fee for the days used. A self-serve checkout charges that invoice. The new plan is not charged or credited for the rest of the period, and no credit note is issued; its base fee starts at the next billing anchor. Before, such a change billed nothing, so that usage and fee were never invoiced. The same applies to `POST /subscriptions` with `proration_behavior: none`.
* An immediate upgrade is payment-gated: checkout returns a provider result when money is due, or `type: "completed"` when nothing is due.

To see the exact amounts before applying a change, preview it with [`POST /subscriptions/preview`](/api/subscription-management). The preview returns `credit_amount_cents`, `debit_amount_cents`, `net_amount_cents`, `amount_due_now_cents` and the policy it applied.

## How a policy shapes checkout

When a customer with an active subscription checks out a different plan, Nozle resolves the policy for the pair and then:

| Policy | Checkout result |
| - | - |
| `end_of_period` timing | `type: "scheduled"` with `transition_direction`, `transition_policy` and `effective_at`. `subscription_id` is the pending subscription. |
| `upgrade` with `immediate` timing | A payment-gated upgrade: a provider result when money is due, or `type: "completed"` when nothing is due. |
| `downgrade` with `immediate` timing | The downgrade is applied and `type: "completed"` is returned. |

Other `422` responses from plan changes:

| Code | Cause |
| - | - |
| `plan_transition_policy_required` | No policy for the pair, and the free-to-paid fallback does not apply. |
| `plan_already_active` | The target is the current plan. |
| `subscription_not_active` | A transition call named a subscription that is not active. Checkout with a terminated subscription ID was treated as a new checkout in testing. |
| `target_plan_is_not_a_downgrade` | A `downgrade` transition was requested on a pair whose policy direction is `upgrade`. |
| `plan_upgrade_invoice_missing` | An upgrade checkout to a plan that bills in arrears; nothing is invoiced up front to pay. |

A new scheduled change while a change is already pending replaces the pending change and returns a new pending subscription ID. Withdrawing the old ID then returns `409 pending_subscription_changed`. A paid upgrade checkout does not replace or withdraw a pending downgrade: it is refused with `409 subscription_change_pending`. Withdraw the pending change first. Scheduling a change cancels an open upgrade checkout. See [Paid upgrades: current limits](/api/create-checkout#paid-upgrades-current-limits).


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