---
title: "Create Checkout Session"
method: POST
path: "/api/v1/billing/checkout"
tags: ["billing"]
---

# Create Checkout Session

`POST /api/v1/billing/checkout`

## Request

- Base URL: `https://api.badgeia.com`
- URL: `https://api.badgeia.com/api/v1/billing/checkout`
- Auth: none declared

Start a paid subscription, or move an existing one to a different plan.

Three outcomes:

  * ``DevUpgradeResponse`` — dev mode (no Stripe key): instant Pro upgrade.
  * ``PlanChangeResponse`` — the customer already holds a billable Stripe
    subscription, so the requested tier/cadence is applied to THAT
    subscription and no Checkout session exists. See
    ``_select_plan_change_target``.
  * ``CheckoutResponse`` — no live subscription: a normal Stripe Checkout
    session to redirect to.

The middle case is the whole point. Stripe Checkout in ``mode:
subscription`` can only ever CREATE a subscription, so sending a paying
customer through it to change tiers charges them twice and leaves two
subscriptions' webhooks fighting over one ``plan`` column.

## Request body

- CheckoutRequest
  - `price_id` string
  - `annual` boolean
  - `plan` 'pro' | 'team', nullable

## Response `200`

Successful Response

- union
  - CheckoutResponse
    - `checkout_url` string, required
  - DevUpgradeResponse
    - `dev_mode` boolean
    - `plan` string, required
    - `user` UserRead, required
      - `email` string, email, required
      - `name` string, nullable
      - `avatar_url` string, nullable
      - `id` string, uuid, required
      - `username` string, nullable
      - `auth_provider` string, required
      - `plan` string, required
      - `has_billing_account` boolean
      - `plan_interval` string, nullable
      - `email_verified` boolean
      - `trial_ends_at` string, date-time, nullable
      - `is_active` boolean, required
      - `created_at` string, date-time, required
      - `updated_at` string, date-time, required
      - `notify_run_complete` boolean, required
      - `notify_score_change` boolean, required
      - `notify_rank_change` boolean, required
      - `notify_weekly_digest` boolean, required
      - `notify_lifecycle_email` boolean
      - `notify_regression` boolean
      - `pmf_survey_last_shown` string, date-time, nullable
      - `marketplace_payout_email` string, nullable
      - `is_admin` boolean
      - `deletion_requested_at` string, date-time, nullable
      - `deletion_due_at` string, date-time, nullable
      - `arena_run_count` integer
      - `monthly_run_count` integer
      - `agent_count` integer
      - `team_count` integer
  - PlanChangeResponse: Returned when an existing paid subscription was MODIFIED, not replaced. A customer who already pays us and picks a different tier or cadence never reaches Stripe Checkout — there is no session and no redirect, because Checkout can only create a second subscription. The change is applied to the subscription they already have and this response reports the result. ``plan_changed`` is the frontend's branch signal, deliberately a distinct field rather than a reuse of ``dev_mode``: this is a real, live, money- moving change and labelling it "dev mode" would be a lie that some future reader acts on. ``lib/billing.ts`` treats it the way it treats ``dev_mode`` — no redirect, refresh the user — so both /pricing and /settings already do the right thing with it.
    - `plan_changed` boolean
    - `dev_mode` boolean
    - `payment_pending` boolean
    - `payment_url` string, nullable
    - `plan` string, required
    - `interval` string, nullable
    - `previous_plan` string, nullable
    - `user` UserRead, required
      - `email` string, email, required
      - `name` string, nullable
      - `avatar_url` string, nullable
      - `id` string, uuid, required
      - `username` string, nullable
      - `auth_provider` string, required
      - `plan` string, required
      - `has_billing_account` boolean
      - `plan_interval` string, nullable
      - `email_verified` boolean
      - `trial_ends_at` string, date-time, nullable
      - `is_active` boolean, required
      - `created_at` string, date-time, required
      - `updated_at` string, date-time, required
      - `notify_run_complete` boolean, required
      - `notify_score_change` boolean, required
      - `notify_rank_change` boolean, required
      - `notify_weekly_digest` boolean, required
      - `notify_lifecycle_email` boolean
      - `notify_regression` boolean
      - `pmf_survey_last_shown` string, date-time, nullable
      - `marketplace_payout_email` string, nullable
      - `is_admin` boolean
      - `deletion_requested_at` string, date-time, nullable
      - `deletion_due_at` string, date-time, nullable
      - `arena_run_count` integer
      - `monthly_run_count` integer
      - `agent_count` integer
      - `team_count` integer

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/badgeia/apis/badge.md) · [All operations](https://skmtc.dev/badgeia/apis/badge/llms.txt) · [OpenAPI document](https://skmtc.dev/badgeia/apis/badge/revisions/7e7f7fee8b77?raw)
