---
title: "Start a plan checkout"
method: POST
path: "/billing/checkout"
---

# Start a plan checkout

`POST /billing/checkout`

## Request

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

Creates a Paddle checkout transaction for the requested paid plan and billing interval, and returns the transaction id and a client token for the dashboard to open with Paddle.js as an overlay. The trial is once per account, ever: the backend checks whether the account has any billing history — including a canceled subscription — and selects a trial or no-trial price variant accordingly, so only a genuinely first-ever checkout gets the 14-day card-required trial. The plan change itself lands asynchronously via Paddle's webhook after payment; the dashboard polls GET /me until it appears. Returns 400 `validation_failed` when billing is not configured on this deployment, when the plan/interval is unknown, when the account already holds the requested plan, or when it already has a live subscription (a subscriber switches plan through POST /billing/change-plan instead).

## Request body

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

## Response `200`

The Paddle checkout transaction for the dashboard to open with Paddle.js.

- object: Uniform success envelope: the payload is always under `data`.
  - `data` BillingCheckoutResponse, required: A Paddle checkout transaction for the dashboard to open with Paddle.js as an overlay, rather than a URL to redirect to.
    - `transactionId` string, required: The Paddle transaction id to pass to Paddle.js's Checkout.open().
    - `clientToken` string, required: Paddle's public client-side token. Safe to expose in the response; only ever handed to an authenticated account that is actually upgrading.
    - `environment` 'sandbox' | 'live', required: Which Paddle environment the token belongs to. Paddle.js defaults to production and rejects a sandbox client token, so a client must call Paddle.Environment.set('sandbox') before Paddle.Initialize when this is "sandbox".

## 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.
- `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)
