API reference

Billing

Plans, checkout and the subscription.

  1. POST/v1/billing/checkout
  2. GET/v1/billing/checkout/{id}
  3. POST/v1/billing/demo
  4. GET/v1/billing/plans
  5. POST/v1/billing/portal
  6. GET/v1/billing/slug/check
  7. POST/v1/billing/subscription
  8. POST/v1/billing/subscription/keep
POST/v1/billing/checkout

Start 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>.

Request body · CreateCheckoutDto
FieldTypeWhat it is
slug requiredstringSubdomain 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 requiredstringExample: "Acme Inc"
taxIdstringAn 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"
Responses
StatusBodyWhen
201CheckoutSessionDto
201 body · CheckoutSessionDto
FieldTypeWhat it is
checkoutId requiredstring
clientSecret requiredstringFor Stripe Elements to confirm in the browser.
plan requiredstring
interval requiredstring
currency required"usd" | "gbp" | "eur"
amountCents requirednumberThe amount, in the currency's minor units.
GET/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>.

Parameters
NameWhereTypeWhat it is
id requiredpathstring
Responses
StatusBodyWhen
200CheckoutStatusDto
200 body · CheckoutStatusDto
FieldTypeWhat it is
id requiredstring
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 requiredstring
interval requiredstring
currency required"usd" | "gbp" | "eur"
organizationCheckoutOrganizationDto | nullNull until the webhook has provisioned the workspace.
organization.name requiredstring
organization.slug requiredstring
organization.url requiredstringThe workspace's own sign-in URL.
POST/v1/billing/demo

Request 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>.

Request body · StartDemoDto
FieldTypeWhat it is
slug requiredstringSubdomain for the workspace. Lowercase letters, numbers and single hyphens. Example: "acme"
organizationName requiredstringNames the demo workspace. Example: "Acme Inc"
Responses
StatusBodyWhen
201DemoWorkspaceDto
409—This user has already had a demo
201 body · DemoWorkspaceDto
FieldTypeWhat it is
id requiredstring
name requiredstring
slug requiredstring
plan requiredstring
trialEndsAtstring | null
url requiredstringThe workspace's own sign-in URL.
GET/v1/billing/plans

Plans and prices (no sign-in needed)

Needs: Nothing. This route is public.

Responses
StatusBodyWhen
200PlanCatalogueDto
200 body · PlanCatalogueDto
FieldTypeWhat it is
demoDays requirednumberHow many days a requested demo lasts.
currencies requiredstring[]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 requiredPlanDto[]
plans[].key requiredstring
plans[].name requiredstring
plans[].monthlyCents requirednumberUSD monthly price. Kept for readers written before multi-currency.
plans[].annualCents requirednumberUSD annual price. Kept for readers written before multi-currency.
plans[].prices requiredobjectList price per offered currency, keyed by ISO code ("usd", "gbp", "eur"). Contains exactly the currencies named by currencies on the catalogue.
plans[].domainsnumber | null
plans[].seats requirednumber
plans[].purchasable requiredboolean
POST/v1/billing/portal

Open 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>.

Request body · PortalSessionRequestDto
FieldTypeWhat it is
organizationId requiredstring (uuid)The workspace whose billing to open. Must be owned.
returnUrlstringWhere 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"
Responses
StatusBodyWhen
200PortalSessionDto
200 body · PortalSessionDto
FieldTypeWhat it is
url requiredstringStripe-hosted, short-lived. Redirect, do not store.
GET/v1/billing/slug/check

Is 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>.

Parameters
NameWhereTypeWhat it is
slug requiredquerystring
Responses
StatusBodyWhen
200SlugCheckDto
200 body · SlugCheckDto
FieldTypeWhat it is
slug requiredstringThe slug that would be created, after normalisation.
available requiredboolean
reasonstring | nullWhy it cannot be used: too_short | too_long | invalid_characters | edge_hyphen | double_hyphen | reserved | taken. Null when available.
message requiredstringSafe to show verbatim. Empty when available.
suggestions requiredstring[]Free alternatives. Empty when the slug is available.
POST/v1/billing/subscription

Change 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>.

Request body · ChangePlanDto
FieldTypeWhat it is
organizationId requiredstring (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.
taxIdstringAn 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"
Responses
StatusBodyWhen
200ChangePlanResultDto
200 body · ChangePlanResultDto
FieldTypeWhat 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 requiredstring
interval requiredstring
currency required"usd" | "gbp" | "eur"The currency actually used — the subscription's own, if it has one.
amountCents requirednumberWhat the new plan costs at this interval, in that currency's minor units.
checkoutIdstring | nullPoll GET /v1/billing/checkout/{id} with this. Null when updated.
clientSecretstring | nullFor Stripe Elements. Null when updated — there is nothing to pay.
unpublishedDomainsstring[]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.
POST/v1/billing/subscription/keep

Withdraw 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>.

Request body · KeepSubscriptionDto
FieldTypeWhat it is
organizationId requiredstring (uuid)
Responses
StatusBodyWhen
200KeepSubscriptionResultDto
200 body · KeepSubscriptionResultDto
FieldTypeWhat it is
status required"kept"

Next groupNotifications