public
POST /v1/public/checkout
Check out as a guest — create the account and place the order.
Authentication
This endpoint is public. It takes no credential and no organisation — it is what our own marketing site and AI answer engines read.
This endpoint takes no organisation id. Your key already identifies the organisation it belongs to, and the response is scoped to it.
Try it
Replace anything in angle brackets with your own values, and the key placeholder with a key from your dashboard.
curl -X POST https://api.zinndigital.com/v1/public/checkout \
-H "Content-Type: application/json" \
-d '{ "cart_token": <string>, "email": <string>, "billing": <PublicBilling> }'Signed in? The API console in your dashboard fills in your real organisation id and your own key, and runs the request against the live API so you can see the actual response. Open this endpoint in the API console
Details
Converts an anonymous cart into a real tenant: it creates the organization and the user at this point, captures the billing details, and places an immutable order. Availability is re-verified live against the registrar first, so a name taken while the customer was typing aborts the whole call (409) with nothing created. Tax is computed against billing.country (§2.8). It never takes a card, and what it does next depends entirely on the total. The response is {order_id, org_id, next} plus the order's own currency, subtotal_minor, tax_minor and total_minor: - "payment" — the order has something to pay and is left pending_payment. Taking payment is the authenticated step that follows, because a brand-new organization has no payment mandate on file. - "done" — the total is zero (a free trial, a zero-priced plan, a 100% discount), so the order is settled and provisioned inside this call: a zero Payment is recorded, the order becomes paid, and the fulfilment workflow starts. No gateway is contacted — a brand-new organization has none to resolve, and there is nothing to charge. Owner ruling 2026-08-09: a free trial takes no card and starts immediately. ⛔ Between that ruling and 2026-08-12 this endpoint answered "done" and performed none of it — the order stayed pending_payment with no payment, no fulfilment and no workflow, so the customer was told their trial had started and no site was ever built. Anything that keys off next should key off it alone; the order state now follows it. Idempotent on the cart. A double-submit — concurrent or an hour later — produces one organization, one user and one order; the repeat returns the same ids. There is no Idempotency-Key header to send: the cart token is the key. If the email already has an account the call is refused with 409 and nothing is created: the purchase must be completed signed in, so that an unauthenticated request can never attach an order to somebody else's tenant. The cart stays open. Cross-origin callers. Besides zinndigital.com, this route answers the browsers of the hosting sites' on-page free trial — zinnhosting.com, zinnhosting.co.uk and the zinndigital country sites (engine.api.trial_cors, list generated from the product-sites registry into engine/engine/api/trial_origins.json). Exact origins only, never credentials; the route's own budgets, Turnstile and trial guard apply unchanged (docs/749 §16).
Request body
| Name | Type | Required | What it is |
|---|---|---|---|
cart_token | string | Yes | — |
email | string | Yes | — |
password | string | No | Optional. Omit it and the account is created without a credential, so the customer sets one through the identity provider's own verified flow — which is the safer default, because… |
name | string | No | — |
locale | string | No | — |
turnstile_token | string | No | Q114: the Cloudflare Turnstile token from the sign-up form's challenge (site key from GET /v1/public/quotes/form). Demanded once the signup human-check surface is on; a replay… |
billing | PublicBilling | Yes | The buyer block. Only country is required, because tax is country- and currency-aware and an order with no jurisdiction has no determinable tax; the rest can be completed later… |
registrant | Registrant | No | Who the basket's domains are registered to. Optional on the wire — a basket with no domain needs none — but required whenever the basket holds a domain: without it the call is… |
registry_data | object | No | Q10 — domain_name → the fields that domain's registry demands beyond the registrant (getTldRequirements lists them per extension: ID / tax / company numbers, .us nexus,… |
news_opt_in | boolean | No | The UNTICKED "Send me Zinn® news" box at sign-up / checkout (s1014 DOUBLEOPTIN, docs/1052 §7j). true sends ONE confirmation e-mail with a signed link; nothing is marketing until… |
news_source | string<signup, checkout> | No | Where the box was ticked, for the consent record. Omitted means checkout. |
news_wording | string | No | The version of the sentence shown beside the box (news-v1), for the consent record. An unknown version records no request. |
marketing_opt_out | boolean | No | "Don't email me offers and tips" — the marketing opt-out offered at collection (soft opt-in, PECR reg. 22(3); owner ruling 2026-08-26). true records the same suppression a… |
attribution | object | No | — |
Response
| Name | Type | Required | What it is |
|---|---|---|---|
order_id | Uuid | Yes | UUIDv7 identifier — sortable by creation time (docs/02 §8). |
org_id | Uuid | Yes | The organization created by this call — the customer's new tenant. |
next | string<payment, done> | Yes | What the client should do next. payment — the order exists and is pending_payment; nothing has been charged. done — the order totals zero and there is nothing to charge,… |
currency | string | Yes | ISO 4217 code of the placed order. |
subtotal_minor | integer | Yes | The order's net amount, in minor units. |
tax_minor | integer | Yes | The tax computed for billing.country, in minor units. A guest basket has no country, so this is the first moment the tax exists — show it. |
total_minor | integer | Yes | What the order owes, tax included, in minor units. ⛔ Display THIS, never the basket total, which is pre-tax (D25043). |
Errors this endpoint can return
404 · 409 · 422 · 429 · 503