---
title: "Plans and prices"
method: GET
path: "/v1/billing/plans"
tags: ["Catalogue"]
---

# Plans and prices

`GET /v1/billing/plans`

## Request

- Base URL: `https://api.kawaa.com`
- URL: `https://api.kawaa.com/v1/billing/plans`
- Auth: one of:
  - API key in header `X-Api-Key`
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.kawaa.com/oauth/authorize`, token URL `https://api.kawaa.com/oauth/token`, scopes: `billing:read`)

The plan catalogue. Needs a valid API key like every other route here; `is_current` marks the plan that key's account is on.

Every `price` is quoted in `currency`, which is always USD: this endpoint does not take a currency and does not convert. `billable_currencies` is the separate question of which currencies a subscription can be created in, and `credit_package_currencies` the same question for one-off credit packs. Those two lists differ, so offering a currency from the wrong one produces a checkout the customer cannot complete.

**A white-label sub-account has no plan of its own.** The route is open to sub-accounts (it is the public price list), but such an account spends a credit pool its parent allocated and holds no subscription. It gets `current_plan: "sub_account"` (agreeing with `GET /v1/account`), `is_current: false` on every plan, and a `plan_note` saying where the subscription lives. Do not present a plan as that account's, and expect `403 SUB_ACCOUNT_MANAGED` from every subscription write.

## Response `200`

The plans.

- object
  - `success` unknown, required
  - `data` object, required
    - `plans` object[], required: Every plan, in catalogue order, free first. A sub-account is not in here: it has no plan of its own.
      - `id` 'free' | 'starter' | 'professional' | 'business' | 'enterprise', required: The value `POST /v1/billing/checkout` takes as `plan`.
      - `name` string, required: The display name.
      - `price` number, required: Per `interval`, in `currency`, in whole currency units (not cents). `0` for free.
      - `currency` 'USD' | 'EUR' | 'GBP' | 'BRL' | 'INR', required: Always `USD` today: the catalogue is quoted in one currency. See `billable_currencies` for what a subscription can be created in.
      - `interval` 'month', required: How `price` is billed: every paid plan is billed monthly. It does not say the credits renew; read `credits_renewal` for that. Free has no price, and its credits do not renew.
      - `credits` number, required: The plan's credit grant. On a paid plan it is added each billing period, when that period's invoice is paid (`credits_renewal: "monthly"`); an invoice left unpaid grants nothing. On Free the grant does not renew monthly (`credits_renewal: "one-time"`): 100 credits given when the account is created, and another 100 each time a paid subscription ends (is cancelled or deleted) and the account moves back to Free. An unpaid subscription that moves the account to Free does not grant it: that subscription has not ended. Not a verification count: a cached verification costs 0.5 and a domain search 10.
      - `credits_renewal` 'monthly' | 'one-time': Whether `credits` renews: `monthly` on every paid plan, `one-time` on Free. The same field, with the same values, as `GET /v1/usage/plans` publishes for each plan. Optional, so a client should handle a plan without it.
      - `features` string[], required: Marketing lines for the plan, in display order. Not a machine-readable entitlement list; read `GET /v1/usage/limits` for the enforced numbers.
      - `is_current` boolean, required: True for the plan the calling key's account is on. False on every plan for a white-label sub-account, which is subscribed to none of them.
    - `current_plan` string, required: The account's plan id, repeated here so a client does not have to scan `plans` for `is_current`. `free` when the account has no subscription. `sub_account` for a white-label sub-account, which has no plan of its own; see `plan_note`, and note that no entry in `plans` is marked `is_current` in that case.
    - `plan_note` string: Present only when `current_plan` is not a plan the account is subscribed to: today, only for a white-label sub-account, whose subscription is held by its parent and whose credits are the pool that parent allocated. Same field, and the same purpose, as `plan_note` on `GET /v1/usage/limits`. Absent for every ordinary account.
    - `currency` 'USD' | 'EUR' | 'GBP' | 'BRL' | 'INR', required: The currency every `price` above is quoted in.
    - `billable_currencies` string[], required: The currencies a SUBSCRIPTION can actually be created in, derived from the Stripe prices that exist, so it can be shorter than the supported list and can change at any time. A currency outside it is refused at checkout.
    - `credit_package_currencies` string[], required: The currencies a one-off CREDIT PACK can be bought in. Packs are priced inline at checkout rather than from a stored Stripe price, so this list is not bound by `billable_currencies` and is usually longer.

## Other responses

- `401`: No credential, or one that is not accepted. `code` distinguishes `API_KEY_SUSPENDED` (subscription ended; restartable) from `API_KEY_REVOKED`, `SESSION_EXPIRED` and `OAUTH_TOKEN_EXPIRED` (a connected app's one-hour access token lapsed: refresh it and retry, or sign in again if no refresh token was issued or the refresh is refused).
- `403`: Refused, and **the code says which of three problems it is**, and they need different remedies: - `INSUFFICIENT_SCOPE`: the credential is valid but not scoped for this route. With an API key, issue one that carries the scope named in the message. With an OAuth access token, connect the app again asking for that scope as well as the ones it has, unless the message says a connected app can never be given it; then use an API key with every scope the message names, or have the account owner do it in the dashboard. - `PLAN_UPGRADE_REQUIRED`: the account's plan does not include the feature. **No key of any scope reaches it**, so issuing another is wasted work; the remedy is a plan change. Returned by the verification routes for `options.enrich`, and by the finder, the monitors, the audit log, auto-clean and the white-label routes. - `PLAN_LIMIT_EXCEEDED`: the plan includes the feature and the account is at its ceiling (webhook endpoints, team seats, integrations, auto-clean lists). Delete one or upgrade. - `FORBIDDEN`: refused for a reason none of the above covers. `ErrorCode` is an open set, so branch on the code with a default rather than exhaustively.
- `429`: Too many requests. When the server knows how long to wait, `Retry-After` says so and `error.retry_after_seconds` carries the same number; the `X-RateLimit-*` headers say where you stand.
- `default`: Any status this operation does not list. Kawaa's own errors carry the error envelope. The gateway in front of the API can answer any operation with its own bare body (`GatewayError`): a `503` after about 29 seconds, when the request may still have run and been charged, a `413` for a body over about 6 MB, or a `400` for a path with malformed percent-encoding.

---

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