---
title: "Apply a promotion code to the running subscription"
method: POST
path: "/billing/promotion-code"
tags: ["Billing"]
---

# Apply a promotion code to the running subscription

`POST /billing/promotion-code`

## Request

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

Applies a promotion code, as a customer types it, to the workspace's running subscription (a new subscription takes a code as `promotion_code` on `POST /billing/checkout`). A code is accepted only when it belongs to this deployment, because a Stripe account can be shared with other products: it was created through the operator console, or its coupon is limited to this deployment's Stripe products. An unknown, inactive or foreign code is a `404`. Requires `billing:manage`.

## Request body

- object
  - `code` string, required

## Response `200`

The new billing status

- 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}.
- `403`: Error envelope: {code, title, details}.
- `404`: Error envelope: {code, title, details}.
- `409`: 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)
