Billing
Plans, checkout and the subscription.
/v1/billing/checkoutStart a paid signup
Creates an incomplete Stripe subscription and returns a client secret for Stripe Elements. No workspace is created here — the webhook does that once Stripe confirms the invoice is paid.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| slug required | string | Subdomain for the workspace. Lowercase letters, numbers and single hyphens. Example: "acme" |
| plan required | "starter" | "pro" | "agency" | |
| interval required | "monthly" | "annual" | |
| currency | "usd" | "gbp" | "eur" | Which currency to charge in. Defaults to USD. Refused unless the catalogue lists it, so a client cannot ask for a currency we have no Stripe price for. |
| organizationName required | string | Example: "Acme Inc" |
| taxId | string | An EU or UK VAT number, with its country code, to print on the invoices. Optional. It changes no amount: how tax is charged is decided elsewhere. Example: "DE123456789" |
| Status | Body | When |
|---|---|---|
| 201 | CheckoutSessionDto |
| Field | Type | What it is |
|---|---|---|
| checkoutId required | string | |
| clientSecret required | string | For Stripe Elements to confirm in the browser. |
| plan required | string | |
| interval required | string | |
| currency required | "usd" | "gbp" | "eur" | |
| amountCents required | number | The amount, in the currency's minor units. |
/v1/billing/checkout/{id}Poll a checkout
The browser polls this after confirming payment: provisioning normally happens in the webhook, so "card accepted" and "workspace ready" are seconds apart. If the webhook has not arrived, this asks Stripe directly and finishes a paid checkout.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| id required | path | string |
| Status | Body | When |
|---|---|---|
| 200 | CheckoutStatusDto |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| status required | "pending" | "paid" | "failed" | "canceled" | Provisioning happens in the Stripe webhook, so this stays "pending" for a moment after the card is confirmed. |
| plan required | string | |
| interval required | string | |
| currency required | "usd" | "gbp" | "eur" | |
| organization | CheckoutOrganizationDto | null | Null until the webhook has provisioned the workspace. |
| organization.name required | string | |
| organization.slug required | string | |
| organization.url required | string | The workspace's own sign-in URL. |
/v1/billing/demoRequest a 7-day demo workspace
Explicitly requested — signing up does not create a workspace. Full-featured; limited by time, not capability. One per user.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| slug required | string | Subdomain for the workspace. Lowercase letters, numbers and single hyphens. Example: "acme" |
| organizationName required | string | Names the demo workspace. Example: "Acme Inc" |
| Status | Body | When |
|---|---|---|
| 201 | DemoWorkspaceDto | |
| 409 | — | This user has already had a demo |
| Field | Type | What it is |
|---|---|---|
| id required | string | |
| name required | string | |
| slug required | string | |
| plan required | string | |
| trialEndsAt | string | null | |
| url required | string | The workspace's own sign-in URL. |
/v1/billing/plansPlans and prices (no sign-in needed)
Needs: Nothing. This route is public.
| Status | Body | When |
|---|---|---|
| 200 | PlanCatalogueDto |
| Field | Type | What it is |
|---|---|---|
| demoDays required | number | How many days a requested demo lasts. |
| currencies required | string[] | The currencies this deployment can take payment in, in display order. Only currencies with a Stripe price for every purchasable plan and interval appear here, so anything listed can actually be charged. Example: ["usd","gbp","eur"] |
| plans required | PlanDto[] | |
| plans[].key required | string | |
| plans[].name required | string | |
| plans[].monthlyCents required | number | USD monthly price. Kept for readers written before multi-currency. |
| plans[].annualCents required | number | USD annual price. Kept for readers written before multi-currency. |
| plans[].prices required | object | List price per offered currency, keyed by ISO code ("usd", "gbp", "eur"). Contains exactly the currencies named by currencies on the catalogue. |
| plans[].domains | number | null | |
| plans[].seats required | number | |
| plans[].purchasable required | boolean |
/v1/billing/portalOpen the Stripe billing portal for a workspace
Where the card is replaced, invoices are downloaded and a subscription is cancelled. Owner only. 400 when the workspace has never been to Stripe — a demo has no billing account to open.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| organizationId required | string (uuid) | The workspace whose billing to open. Must be owned. |
| returnUrl | string | Where Stripe returns the customer afterwards. Only paths on the customer area are honoured — anything else falls back to /account/billing rather than being rejected, since this is not something the customer chose. Example: "/account/subscription" |
| Status | Body | When |
|---|---|---|
| 200 | PortalSessionDto |
| Field | Type | What it is |
|---|---|---|
| url required | string | Stripe-hosted, short-lived. Redirect, do not store. |
/v1/billing/slug/checkIs a workspace address available?
For checking as the customer types. Advisory only: the address is checked again when the workspace is created, and could be taken in between.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Name | Where | Type | What it is |
|---|---|---|---|
| slug required | query | string |
| Status | Body | When |
|---|---|---|
| 200 | SlugCheckDto |
| Field | Type | What it is |
|---|---|---|
| slug required | string | The slug that would be created, after normalisation. |
| available required | boolean | |
| reason | string | null | Why it cannot be used: too_short | too_long | invalid_characters | edge_hyphen | double_hyphen | reserved | taken. Null when available. |
| message required | string | Safe to show verbatim. Empty when available. |
| suggestions required | string[] | Free alternatives. Empty when the slug is available. |
/v1/billing/subscriptionChange a workspace’s plan
Re-prices the live subscription when there is one (status: "updated", in effect immediately; an upgrade is charged to the card on file now, and 402 with nothing changed if that charge is declined). When the workspace is a demo there is nothing to re-price, so this returns status: "payment_required" with a client secret — and the workspace only moves once the webhook sees the invoice paid. The workspace keeps its address, data and members either way.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| organizationId required | string (uuid) | The workspace to move. The caller must own it. |
| plan required | "starter" | "pro" | "agency" | |
| interval required | "monthly" | "annual" | |
| currency | "usd" | "gbp" | "eur" | Only honoured when the workspace has no live subscription. An existing subscription keeps its own currency — Stripe cannot change the currency of one — and this field is ignored rather than refused. |
| taxId | string | An EU or UK VAT number, with its country code, to print on the invoices. Optional. It changes no amount: how tax is charged is decided elsewhere. Example: "DE123456789" |
| Status | Body | When |
|---|---|---|
| 200 | ChangePlanResultDto |
| Field | Type | What it is |
|---|---|---|
| status required | "updated" | "payment_required" | updated — the live subscription was re-priced and the change is already in effect. payment_required — the workspace had no subscription (a demo), so nothing changes until clientSecret is confirmed and the invoice is paid. |
| plan required | string | |
| interval required | string | |
| currency required | "usd" | "gbp" | "eur" | The currency actually used — the subscription's own, if it has one. |
| amountCents required | number | What the new plan costs at this interval, in that currency's minor units. |
| checkoutId | string | null | Poll GET /v1/billing/checkout/{id} with this. Null when updated. |
| clientSecret | string | null | For Stripe Elements. Null when updated — there is nothing to pay. |
| unpublishedDomains | string[] | Hostnames whose banner was taken offline because the new plan does not cover them. A downgrade shrinks the domain allowance; the oldest domains keep serving and the rest are unpublished. Empty on every upgrade. Nothing is deleted — publishing again after an upgrade restores exactly what was live. |
/v1/billing/subscription/keepWithdraw a scheduled cancellation
A subscription cancelled in the billing portal runs to the end of the paid period and then ends. Until that moment this undoes it: the subscription renews as before and nothing is charged now. Owner only. 409 once the subscription has already ended.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| organizationId required | string (uuid) |
| Status | Body | When |
|---|---|---|
| 200 | KeepSubscriptionResultDto |
| Field | Type | What it is |
|---|---|---|
| status required | "kept" |
Next groupNotifications