---
title: "Record a completed plan checkout now (owners and admins)"
method: POST
path: "/billing/checkout:confirm"
tags: ["Billing"]
---

# Record a completed plan checkout now (owners and admins)

`POST /billing/checkout:confirm`

## Request

- Base URL: `https://api.customdomain.ai/v1`
- URL: `https://api.customdomain.ai/v1/billing/checkout:confirm`
- Auth: HTTP bearer

Called when Stripe returns the browser from a completed Checkout, so the subscription is recorded at once instead of when Stripe's `checkout.session.completed` event arrives. The session is read from Stripe, never trusted from the caller: it must belong to the caller's workspace (another workspace's session is a `404`) and be complete and paid (a trial's "no payment required" counts); otherwise `confirmed` is false and nothing changes. Idempotent with the webhook. Requires `billing:manage`.

## Request body

- object
  - `session_id` string, required

## Response `200`

Whether the checkout was recorded, and the billing status after

- object
  - `confirmed` boolean
  - `session_status` string
  - `payment_status` string
  - `billing` BillingStatus
    - `tenant_id` string
    - `state` 'trialing' | 'active' | 'past_due' | 'grace' | 'blocked' | 'comped' | 'canceled' | 'legacy_free': A workspace's billing state, evaluated from its record and the clock. `trialing` and `active` are a running Stripe subscription; `past_due` is a failed payment Stripe is still retrying (nothing is refused); `grace` is a legacy Free workspace (or an operator extension) inside its grace period; `legacy_free` is a Free workspace whose grace has not started; `comped` is an operator comp; `blocked` and `canceled` refuse new connections (`402 billing_required`) while live domains keep working. A new workspace (`blocked`, reason `setup_required`) is refused only once its first domain is connected: see `first_domain_free`.
    - `reason` string
    - `restricted` boolean
    - `first_domain_free` boolean: Present only when true: a new workspace on the first domain free rule (see `GET /billing/notice`).
    - `free_domains` integer: Present with `first_domain_free`: how many domains the workspace may connect before it has a subscription.
    - `attention` boolean
    - `setup_required` boolean
    - `days_left` integer
    - `price_cents` integer: What the workspace pays per month (the Stripe amount, else the catalog price).
    - `currency` string
    - `trial_ends_at` string, date-time, nullable
    - `grace_started_at` string, date-time, nullable
    - `grace_ends_at` string, date-time, nullable
    - `current_period_end` string, date-time, nullable
    - `cancel_at_period_end` boolean
    - `ended_at` string, date-time, nullable
    - `comped` boolean
    - `comped_until` string, date-time, nullable
    - `payment_failed` boolean
    - `payment_failed_at` string, date-time, nullable
    - `has_billing_account` boolean
    - `portal_available` boolean
    - `can_change_plan` boolean
    - `mode` 'test' | 'live' | ''
    - `plan` object
      - `id` 'starter' | 'startup' | 'growth' | 'premium' | 'enterprise' | 'free': `free` is retired: legacy workspaces still carry it until they subscribe, and it cannot be bought or assigned.
      - `name` string
      - `price_cents` integer
      - `public_price` boolean
      - `domains_per_year` integer
      - `domains_per_month` integer
      - `hard_cap` boolean
      - `retired` boolean
    - `next_charge` object, nullable
      - `at` string, date-time
      - `amount_cents` integer
      - `currency` string
    - `checkout` object: What a checkout started now would give.
      - `available` boolean
      - `trial_days` integer
      - `trial_end` string, date-time, nullable
      - `first_charge_at` string, date-time
      - `default_plan` 'starter' | 'startup' | 'growth' | 'premium' | 'enterprise' | 'free': `free` is retired: legacy workspaces still carry it until they subscribe, and it cannot be bought or assigned.
    - `usage` object
      - `used` integer
      - `quota` integer
      - `period_start` string, date-time
      - `period_end` string, date-time
    - `payment_method` object, nullable: The payment method the workspace is charged on, or null when Stripe holds none. A card has its brand, last four digits and expiry, and names the wallet when it was paid through Apple Pay or Google Pay. A Link account has the email of the account (Stripe never shows the card behind it). Cash App Pay has only its type.
      - `type` 'card' | 'link' | 'cashapp'
      - `brand` string
      - `last4` string
      - `exp_month` integer
      - `exp_year` integer
      - `wallet` 'apple_pay' | 'google_pay': The wallet a card was paid through, when it was.
      - `email` string: The email of the Link account (type link).

## Other responses

- `400`: Error envelope: {code, title, details}.
- `401`: Error envelope: {code, title, details}.
- `403`: Error envelope: {code, title, details}.
- `404`: Error envelope: {code, title, details}.
- `503`: Error envelope: {code, title, details}.

---

[API](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api.md) · [All operations](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/llms.txt) · [OpenAPI document](https://skmtc.dev/customdomain/apis/customdomain-control-plane-api/revisions/34d89cb7a7cd?raw)
