---
title: "Switch an existing subscription to another plan"
method: POST
path: "/v2/workspaces/{id}/billing/plan"
tags: ["Billing"]
---

# Switch an existing subscription to another plan

`POST /v2/workspaces/{id}/billing/plan`

## Request

- Base URL: `https://api.infolang.ai`
- URL: `https://api.infolang.ai/v2/workspaces/{id}/billing/plan`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer

Moves the workspace owner's EXISTING subscription to another plan, up or down, by modifying it in place. Does not create a second subscription and does not return a URL: the card is already on file, so the change applies immediately.

Prorated with `create_prorations` in both directions — the unused remainder of the old plan is credited and the used remainder of the new one charged, both on the NEXT invoice. No card is charged today. A DOWNGRADE takes effect at once, so an account holding more than the smaller plan includes is immediately over its ceiling; say so before the customer confirms.

`enterprise` is not a valid target and an `enterprise` account cannot be switched off this way — it is quoted and hand-provisioned. `free` is not a target either: ending a subscription is the cancel route, which keeps the paid plan until the period ends.

Same subject rules as checkout: the billing subject is the workspace OWNER on every lane, and a session-lane caller who is not the owner is refused `403`.

The response reports what STRIPE accepted. `plan` on the billing summary is written by the `customer.subscription.*` webhook a moment later, so a summary fetched in the same breath may still report the old plan — render the value returned here and re-fetch the summary shortly after, rather than treating the lag as a failure.

Requires the `billing:write` scope. Accepts all three lanes: a session JWT (bounded by the caller's role on this workspace) or an `il_*` machine credential whose own scope set, workspace set and validity window cover it — both ceilings apply and neither can raise the other, so a credential carrying `billing:write` is still refused where the role does not permit it.

A credential with no scope record resolves to `LEGACY_DEFAULT`, which does NOT include `billing:write` — mint a key with it explicitly (`POST /v2/workspaces/{id}/keys`).

## Path parameters

- `id` string, required

## Request body

- object
  - `plan` string, required

## Response `200`

The subscription after the switch

- object
  - `plan` string, required
  - `cancel_at_period_end` boolean, required
  - `current_period_end` string, nullable, required

## Other responses

- `401`: Missing/invalid credential, or `credential_expired` — the credential is outside its `[not_before, expires_at)` window and must be replaced
- `403`: `insufficient_scope` — the credential's own scope set, or the caller's role on this workspace, does not cover the scope this route requires (the missing scope is named in the message). Or `forbidden` — the credential is not scoped to this workspace, or holds no grant on it
- `409`: no_subscription — this account holds no live Stripe subscription, so there is nothing to switch or cancel. Start one with the checkout route.
- `422`: invalid_request — no `plan` in the body, an unknown plan, or a plan that is not a self-serve switch target (`enterprise`, `free`).
- `500`: internal_error — unexpected failure (e.g. billing backend misconfigured or unreachable)
- `503`: not_configured — self-serve billing is not available on this deployment: either no payment processor is wired, or its prices do not reconcile with the published catalog. Returned INSTEAD of a checkout URL, because the alternatives were a link to a domain that does not resolve and a checkout that charges an amount the page never showed. Mirrors `self_serve_checkout` on both summaries exactly — same evaluation, same cache.

---

[API](https://skmtc.dev/infolang/apis/infolang-api.md) · [All operations](https://skmtc.dev/infolang/apis/infolang-api/llms.txt) · [OpenAPI document](https://skmtc.dev/infolang/apis/infolang-api/revisions/d04bf6de72c8?raw)
