---
title: "Switch an existing subscription's plan or interval"
method: POST
path: "/billing/change-plan"
---

# Switch an existing subscription's plan or interval

`POST /billing/change-plan`

## Request

- Base URL: `https://app.yoping.me/api/v1`
- URL: `https://app.yoping.me/api/v1/billing/change-plan`
- Auth: API key in cookie `yp_session`

Switches a live subscription between Pro and Team, and between monthly and annual, by updating its price at Paddle. The difference is prorated and charged or credited immediately. Paddle's customer portal cannot change plans, so this is the only self-serve path; a customer without a subscription uses POST /billing/checkout instead, and that endpoint refuses an account that already has a live one.

The no-trial price variant is always used: an account with a live subscription has billing history by definition.

Like checkout, the plan flip lands asynchronously via Paddle's webhook — this returns `status: "updating"` and the dashboard polls GET /me until `billing.plan` and `billing.interval` match.

Returns 400 `validation_failed` when billing is not configured, when the plan/interval is unknown, when the account has no subscription to change (including a comped plan granted by an operator), when the subscription is past due or already cancelling, or when it is already on the requested plan and interval. Returns 422 `plan_limit` when the account's current usage exceeds the target plan's limits; its `error.details.blockers` lists each one so the customer can be told what to trim first.

## Request body

- BillingCheckoutRequest
  - `plan` 'pro' | 'team', required
  - `interval` 'monthly' | 'annual', required

## Response `200`

The switch was accepted at Paddle; the plan flip follows via webhook.

- object: Uniform success envelope: the payload is always under `data`.
  - `data` BillingChangePlanResponse, required
    - `plan` 'pro' | 'team', required: The plan the subscription is moving to.
    - `interval` 'monthly' | 'annual', required: The billing interval it is moving to.
    - `status` 'updating', required: Always `updating`: Paddle accepted the change and the webhook will apply it.

## Other responses

- `400`: Invalid input (code `validation_failed`): malformed JSON, a missing or out-of-range field, bad pagination parameters, or a rejected token.
- `401`: Not authenticated (code `unauthorized`): missing, invalid, or expired session; invalid login credentials; or a Bearer token, which this surface rejects.
- `403`: Forbidden. Code `forbidden` for a cross-origin (CSRF) request blocked by origin validation or for API-key management attempted without a browser session; `account_suspended` when a suspended account attempts a mutation; `account_deleted` when an account in its deletion grace window calls a non-exempt route.
- `422`: A plan or account cap was hit (code `plan_limit`): monitor cap, distinct-target-host cap, or API-key cap.
- `429`: Too many requests (code `rate_limited`). A global per-IP budget of 300 requests/minute (burst 60) applies to every endpoint; login, signup, and test alerts have tighter dedicated limits.

---

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