---
title: "Billing Plan"
method: POST
path: "/me/billing/plan"
---

# Billing Plan

`POST /me/billing/plan`

## Request

- Base URL: `https://api.xhostd.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.xhostd.com/me/billing/plan`
- Auth: none declared

Change the signed-in user's plan at the payment provider.

ONE route carries both directions, because the SERVER decides what a
target tier means. A move to ``basic`` is a cancel, because ``basic``
is the free tier and no provider sells it. A move to another paid
tier is a price change. The route refuses an agent-registration-only
tier with 400, because no subscription grants one.

Bearer-authed like the billing routes above: the route reads
``caller.user_id`` and acts on that user alone. It also demands
``billing:write``, which the console session mint alone grants, so an
agent credential answers 403. It is on the PUBLIC app on purpose. The
internal listener authenticates the console process, not the person,
so a route there would let a console fault move another user's money.

The route acts on the caller's ONE granting subscription. It refuses
when the caller has none, and it refuses when the caller has two or
more: a silent pick would change one subscription and leave the other
to charge. For a subscription that is not cancelled, a target that
equals the subscription's current tier calls the provider not at all,
which stops a double proration on a double submit.

A cancelled subscription still grants its tier until ``ends_at``, so
it is the caller's one live subscription. The provider refuses a
price change on it while the scheduled cancel exists, so the route
reads the target differently:

- The current tier keeps the plan: the route removes the scheduled
  cancel, and the subscription renews on ``ends_at``.
- ``basic`` calls the provider not at all, because the subscription
  already ends at ``ends_at``.
- Another paid tier answers 409 and calls the provider not at all.
  The user keeps the plan first, and then changes it.

The answer's ``action`` is one of ``unchanged`` (no provider call),
``cancelled``, ``changed``, or ``kept``. Each request makes at most one
provider call.

The route passes the direction it already ranked, and each provider
turns that into its own proration flags: an upgrade charges the
difference now, so the user pays for the bigger machine at once, and
a downgrade credits it onto the next invoice. A downgrade must never
produce a charge.

The route writes NO database field. ``users.plan`` stays
projection-only, and the provider's webhook lands seconds later with
the new state. The route also does not wait for an active
``plan_apply`` job: the provider owns the money, and a refusal of a
paid upgrade is worse than a late apply.

## Headers

- `authorization` string, nullable

## Request body

- PlanChangeRequest
  - `tier` string, required

## Response `200`

Successful Response

- unknown

## Other responses

- `422`: Validation Error

---

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