# Change the organization's Ownrecord plan

> The platform_billing.change_plan operation: POST /api/v1/platform_billing/plan in the REST API and the platform_billing_change_plan tool in the MCP server.

Moves the organization to another Ownrecord plan (`starter`, `growth` or `scale`; `sandbox` to go back to the
free plan) and billing interval (`month` or `year` where the plan has a yearly price):

- **Upgrade with a card on file:** applies now, invoiced at once with the unused time credited; the old plan
  stays until the payment succeeds.
- **Upgrade without a card** (for example from `sandbox`): the answer is `outcome: checkout` with `checkoutUrl`,
  Ownrecord's own checkout, where you save a card and pay the first invoice. The plan changes when it is paid.
- **Downgrade:** scheduled for the end of the current period (`outcome: scheduled`), with no credit.

Plans are priced in EUR, CHF and USD: pass `currency`, or leave it out to keep a running paid plan's currency
(or, from the free plan, take the suggestion for your country: CHF for Switzerland and Liechtenstein, EUR for
the EU, USD otherwise). A running paid plan keeps its currency. Every paid plan includes the volume fee on live
payments, billed monthly in arrears in the plan's currency. Invoices come from Ownrecord's entity, Etna Labs AG
in Switzerland, with tax (Swiss VAT for Swiss customers, reverse charge for businesses abroad with a valid VAT
ID). Only an org_owner or billing_admin can change the plan. Dry-run it to see what happens without changing
anything.

## At a glance

- **REST:** `POST /api/v1/platform_billing/plan`
- **MCP tool:** `platform_billing_change_plan`
- **Operation ID:** `platform_billing.change_plan`
- **Scope:** Organization
- **Permission:** `platform_plan:admin`
- **Risk tier:** `financial`
- **Dry run:** Supported (send `Ownrecord-Dry-Run: true`)
- **Idempotency:** `Idempotency-Key` required
- **Modes:** Test and live
- **Authentication:** API key with the `platform_plan:admin` scope
- **Since:** M12

## Request

### Body

A JSON object (`Content-Type: application/json`):

| Field      | Type                   | Required | Description                                                                                      |
| ---------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `plan`     | string                 | Yes      | The plan key, e.g. `growth`.                                                                     |
| `interval` | one of `month`, `year` | No       | The billing interval of the plan's fee.                                                          |
| `currency` | string                 | No       | The subscription's currency: EUR, CHF or USD (default: the running plan's, else your country's). |

## Response

`200 OK` with a JSON object:

| Field            | Type                                      | Description |
| ---------------- | ----------------------------------------- | ----------- |
| `object`         | `"platform_plan_change"`                  |             |
| `orgId`          | string                                    |             |
| `plan`           | string                                    |             |
| `interval`       | one of `month`, `year`                    |             |
| `currency`       | string                                    |             |
| `outcome`        | one of `changed`, `scheduled`, `checkout` |             |
| `subscriptionId` | string                                    |             |
| `effectiveAt`    | date-time or null                         |             |
| `checkoutUrl`    | string or null                            |             |

## Examples

### Upgrade from Sandbox to Growth

No card is on file, so the answer is Ownrecord's checkout.

```http
POST /api/v1/platform_billing/plan
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "plan": "growth",
  "interval": "month"
}
```

Response (`200`):

```json
{
  "object": "platform_plan_change",
  "orgId": "org_01jbz3m4k5n6p7q8r9s0t1v2w3",
  "plan": "growth",
  "interval": "month",
  "currency": "EUR",
  "outcome": "checkout",
  "subscriptionId": "sub_01jbz3m4k5n6p7q8r9s0t1v2w6",
  "effectiveAt": null,
  "checkoutUrl": "https://ownrecord.io/c/ownrecord/cs_01jbz3m4k5n6p7q8r9s0t1v2w9?key=…"
}
```

### Upgrade a Swiss organization to Growth in Swiss francs

```http
POST /api/v1/platform_billing/plan
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "plan": "growth",
  "interval": "month",
  "currency": "CHF"
}
```

Response (`200`):

```json
{
  "object": "platform_plan_change",
  "orgId": "org_01jbz3m4k5n6p7q8r9s0t1v2w3",
  "plan": "growth",
  "interval": "month",
  "currency": "CHF",
  "outcome": "checkout",
  "subscriptionId": "sub_01jbz3m4k5n6p7q8r9s0t1v2w6",
  "effectiveAt": null,
  "checkoutUrl": "https://ownrecord.io/c/ownrecord/cs_01jbz3m4k5n6p7q8r9s0t1v2w9?key=…"
}
```

### Downgrade to Starter at the end of the period

```http
POST /api/v1/platform_billing/plan
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "plan": "starter",
  "interval": "month"
}
```

Response (`200`):

```json
{
  "object": "platform_plan_change",
  "orgId": "org_01jbz3m4k5n6p7q8r9s0t1v2w3",
  "plan": "starter",
  "interval": "month",
  "currency": "EUR",
  "outcome": "scheduled",
  "subscriptionId": "sub_01jbz3m4k5n6p7q8r9s0t1v2w5",
  "effectiveAt": "2026-11-01T00:00:00.000Z",
  "checkoutUrl": null
}
```

## Errors

| Code                                                                                                                   | HTTP status | When                                                                                               | What to do                                                                                                                                                   |
| ---------------------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)                                 | 400         | The plan key, interval or currency is missing or malformed.                                        | Fix the fields listed in `details.issues` and send the request again.                                                                                        |
| [`platform_plan_unknown`](https://ownrecord.io/docs/reference/errors.md#platform_plan_unknown)                         | 400         | There's no self-serve plan with this key, or it has no price for the interval or currency.         | Pick one of the plans `platform_billing.get` lists under `plans` (enterprise is arranged with sales; internal is never sold).                                |
| [`platform_plan_currency_fixed`](https://ownrecord.io/docs/reference/errors.md#platform_plan_currency_fixed)           | 409         | A paid plan is running in another currency: it keeps its currency (move to the free plan first).   | Choose the plan in the currency it is billed in, or move to the free plan first and then choose the plan in the new currency.                                |
| [`platform_plan_unchanged`](https://ownrecord.io/docs/reference/errors.md#platform_plan_unchanged)                     | 409         | The organization is already on this plan and interval.                                             | Choose another plan or interval, or leave the plan as it is.                                                                                                 |
| [`platform_plan_change_pending`](https://ownrecord.io/docs/reference/errors.md#platform_plan_change_pending)           | 409         | An upgrade is still waiting for its payment in a checkout.                                         | Finish the checkout you started (its link is on the billing page), or wait until it expires (23 hours), then try again.                                      |
| [`platform_billing_details_required`](https://ownrecord.io/docs/reference/errors.md#platform_billing_details_required) | 409         | A paid plan needs the organization's VAT ID: add a legal entity, or billing details in the portal. | Add your legal entity (its VAT ID is used), or enter your billing details and VAT ID in Organization → Billing → billing portal, then change the plan again. |
| [`platform_billing_unavailable`](https://ownrecord.io/docs/reference/errors.md#platform_billing_unavailable)           | 409         | Ownrecord's plans aren't set up on this installation yet.                                          | A platform admin runs `pnpm platform:bootstrap`, which creates the plans, prices and features of data/platform/plans.yaml. Try again afterwards.             |
| [`platform_customer_missing`](https://ownrecord.io/docs/reference/errors.md#platform_customer_missing)                 | 409         | The organization's billing account is still being set up (dry runs).                               | Wait a few seconds and reload the billing page. If it persists, contact support.                                                                             |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)                                 | 403         | Only an org_owner or billing_admin changes the plan.                                               | Ask an organization owner or admin for a role or scope that allows this action.                                                                              |

It can also return these [common errors](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`resource_not_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`approval_required`](https://ownrecord.io/docs/reference/errors.md#approval_required), [`approval_rejected`](https://ownrecord.io/docs/reference/errors.md#approval_rejected), [`approval_expired`](https://ownrecord.io/docs/reference/errors.md#approval_expired), [`stale_approval`](https://ownrecord.io/docs/reference/errors.md#stale_approval), [`approval_policy_denied`](https://ownrecord.io/docs/reference/errors.md#approval_policy_denied), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
