Kaiten

Monetization

Self-Serve Checkout

Selling from your own pages — the three public credentials (publishable key, checkout session, customer session), the anonymous checkout end to end from the Turnstile check to the completion, and what you see and receive once a visitor has bought.

Self-serve checkout is the part of billing that runs in browsers you do not control: your pricing page, your checkout page, your customers' billing page. Kaiten serves them under /api/public/, with three credentials of their own — none of them a token of your organization — and keeps every amount on the server: the page shows what Kaiten computed, the payment provider's page takes the card, and a paid License applies once it is paid.

This page covers the credentials, the public catalogue, and the anonymous checkout: a visitor your backend does not know yet buying a License with an e-mail and nothing else. Sell a License online builds it end to end on Kaiten Sushi Shop, and Checkout covers the React components that draw it. Everything here works where billing is enabled for your organization.

Three credentials

CredentialHeld byIssued byReachesLives
Publishable key pk_Your web page, in its sourceYou — Integrations › Publishable keys, or POST /publishable-keys with write:publishable_keysThe public catalogue, from the browser origins it allows; with anonymous checkout on, the start of a checkoutUntil revoked
Checkout session ksc_One visitor's browser tabKaiten, once, in the answer to the startThat session's read and confirm, and nothing elseUntil 24 hours after its session closes
Customer session kst_One signed-in customer's browserYour backend — POST /customer-sessions with write:customer_sessionsThe /public/session/* routes, for that Customer5 to 60 minutes, 30 by default

A pricing page reads the catalogue with the publishable key. A visitor with no account buys through an anonymous checkout, started with the publishable key and carried on with the session's ksc_. A customer who signs in to your product — to see their invoices, change their add-ons, or buy another License — acts through a customer session your backend mints. The three never stand in for one another: each is refused on the routes of the others, and none carries a scope. How each one is sent, and the refusals they share, are on the authentication page.

Publishable keys

A publishable key is not a secret: it ships in your page's source, and what bounds it is the list of browser origins allowed to send it — at most 50, each https://host[:port], or http:// on localhost, with no path. The comparison is exact: https://www.kaiten-sushi.example does not allow https://kaiten-sushi.example. A browser request from another origin answers 403 PublicAuth.OriginNotAllowed, and an empty list allows no browser at all; a request without an Origin header, such as server-side rendering, is accepted. Each key is limited to 20 requests a second, with bursts of 40, counted on each API replica.

The key is shown once, when it is created; the list then shows its last four characters, when it was last used, and its status — Live, or Revoked. Revocation is final: the key stops on the next request, its Turnstile secret is deleted, and the checkout sessions it started stop authenticating. Once Kaiten Sushi Shop sells online, it holds two live keys: Sushi Shop website, allowed on its two production origins and opening the anonymous checkout, and Staging website, which only reads the catalogue.

The Publishable keys page: Sushi Shop website, Live with the Anonymous checkout badge, allowed on https://kaiten-sushi.example and https://www.kaiten-sushi.example, and Staging website, Live, allowed on its staging origin — each shown by the last four characters of its keyThe Publishable keys page: Sushi Shop website, Live with the Anonymous checkout badge, allowed on https://kaiten-sushi.example and https://www.kaiten-sushi.example, and Staging website, Live, allowed on its staging origin — each shown by the last four characters of its key

The anonymous checkout of a key

A key opens the anonymous checkout only when you allow it, and only behind Cloudflare Turnstile: a visitor of the key's origins can create a Customer, so every start carries a Turnstile token that Kaiten verifies itself. The key's form has a section of its own, Anonymous checkout:

  • Turnstile site key — the public key of a Turnstile widget registered in Cloudflare for your pages' hostnames. The catalogue hands it to the page, which draws the widget with it.
  • Turnstile secret — the secret of the same widget. Kaiten stores it in Vault, beside the payment connector's settings, and never shows it again: a key says only whether one is stored (turnstileSecretConfigured), and editing it offers Replace and Remove. A deployment with no Vault cannot store a secret (422 …VaultNotConfigured), so none of its keys can open the anonymous checkout.
  • Allow anonymous checkout — the switch. It stays off until the key holds both values; over the API, a key with the switch on and a value missing is refused with 422 …TurnstileNotConfigured.
The New publishable key dialog for Sushi Shop website: its two allowed origins, one per line, and the Anonymous checkout section switched on, with a Turnstile site key and the Turnstile secret maskedThe New publishable key dialog for Sushi Shop website: its two allowed origins, one per line, and the Anonymous checkout section switched on, with a Turnstile site key and the Turnstile secret masked

At each start, Kaiten sends the token, the secret and the visitor's address to Cloudflare, and checks the answer twice: the token must be valid, and the hostname Cloudflare reports — where the widget was solved — must be the host of one of the key's allowed origins. Either failure answers 422 CreateCheckoutSession.TurnstileFailed; Cloudflare out of reach answers 503 CreateCheckoutSession.TurnstileUnavailable with Retry-After: 30. The verification address is fixed, self-hosted included: the API needs outbound HTTPS to challenges.cloudflare.com — through HTTPS_PROXY where egress goes through a proxy — and nothing skips the check. On the page, the widget loads from the same host: a Content-Security-Policy must allow https://challenges.cloudflare.com in script-src, frame-src and connect-src.

Over the API, the same three members — anonymousCheckout, turnstileSiteKey, turnstileSecret — go in POST /publishable-keys and PATCH /publishable-keys/{keyId}, where an absent member keeps what is stored and an empty Turnstile value clears it.

The public catalogue

GET /public/catalog, read with the key in X-Kaiten-Publishable-Key, is what a pricing page shows: for each License family you made public, its default published version with its active prices and its user-facing entitlements, and the same for public add-on families. Families are private until you make them public — the List … in the public catalog switch on the Licenses list, or isPublic on PATCH /license-families/{familySlug}. include=none leaves the add-ons out, familySlug narrows the read to one family, and the answer may be cached for 60 seconds.

Each License in it says how it is sold:

MemberMeaning
pricingTypeFREE, PAID, or CUSTOM — priced on request
selfServeWhether a visitor can buy it alone: not CUSTOM, with a default flat-fee price, and either FREE or sold through a payment provider that captures payment methods
selfServeCtaUrlWhere to send a buyer when it is not self-serve — your sales page
trialPeriodDays, requiresPaymentMethodIts trial, and whether signing up captures a payment method first
pricesIts active prices, amounts in minor units (money). A metered price is per unit of usage: never add it into a headline

capabilities says what the page can offer: checkout (a License can be bought through a customer session), paymentMethods (your payment provider captures payment methods), anonymousCheckout and turnstileSiteKey (this key starts anonymous checkouts, and the site key its widget is drawn with — null unless it does). branding.poweredByRemovable says whether your pages may hide "Powered by Kaiten": on Kaiten Cloud, where your organization's License allows it; on a self-hosted installation, always.

Selling online, Kaiten Sushi Shop lists its three families. Starter is FREE, with one price of 0.00 EUR a month; Standard is PAID — 49.00 EUR a month or 490.00 EUR a year, and 0.50 EUR per order above its allowance; Premium is CUSTOM, its selfServeCtaUrl leading to the sales page. A visitor can buy Starter and Standard from the page; Premium is a conversation.

The anonymous checkout, end to end

A visitor your backend does not know buys a self-serve License from your page. Until they confirm — or, when a payment method is needed, until the payment provider's page is done — the only record is the checkout session: no Customer, no Instance, no customer at the payment provider. The completion creates them, in one transaction.

Start

The page starts the checkout with the publishable key and a fresh token from its Turnstile widget:

Member
turnstileTokenRequired. Single-use: every start spends one, a dryRun included
priceId — or licenseSlug with billingPeriodThe price bought: a flat-fee price of a License in the public catalogue, or that version's default flat fee for the period. One form, never both
emailThe buyer's address, at most 254 characters. It becomes the new Customer's billing e-mail
returnUrlWhere the payment provider's page sends the visitor back: https, or http on localhost, on an origin the key allows, at most 2,048 characters
clientReferenceIdOptional. Your own reference, up to 255 characters, echoed on the checkout's events
addOns, voucherCode, trialOptional. Public add-ons that fit the License, a voucher code, and false to decline the License's trial
dryRunCompute what is due and write nothing. email and returnUrl may then be left out

The start is refused, before anything is written, when the key does not open the anonymous checkout (403 CreateCheckoutSession.AnonymousCheckoutDisabled), when the Turnstile check fails (422 .TurnstileFailed, 503 .TurnstileUnavailable, 422 .TurnstileNotConfigured), when the price is not a flat fee of a License in the public catalogue (422 .PriceNotPublic, .InvalidPriceSelection, .PriceNotFlatFee), when the License is priced on request (422 .NotSelfServe), when it is PAID and no connected payment provider captures payment methods in its currency (422 .ProviderCannotCapturePayment), and when an amount is due today but the License's family names no fallback (422 .FallbackRequired, below). The other refusals name the member at fault — .InvalidEmail, .InvalidReturnUrl, .VoucherInvalid, .AddonNotPublic, .QuantityExceedsMax, .CurrencyMismatch — and 429 .RateLimited comes with the limits.

A start answers 201 Created with status: "open": the sessionId, its token — the ksc_, returned this once —, expiresAt 24 hours later, dueToday (the currency, the subtotal, the discount, the total and the lines, in minor units, computed by the server, a trial included), trialEndsAt, trialRefusedReason, and requiresPaymentMethod. checkout_session.v1.started is emitted.

With dryRun, the same computation answers 200 OK with status: "preview" and no session: nothing is written and nothing is emitted. A page uses it to show what is due before it asks for an e-mail — it still spends a Turnstile token and counts against the limits. A preview of Standard, monthly:

curl -X POST "$KAITEN_API/public/checkout-sessions" \
  -H "X-Kaiten-Publishable-Key: $KAITEN_PUBLISHABLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "turnstileToken": "XXXX.DUMMY.TOKEN.XXXX",
    "licenseSlug": "standard",
    "billingPeriod": "MONTHLY",
    "dryRun": true
  }'
{
  "status": "preview",
  "dueToday": {
    "currency": "EUR",
    "subtotal": 4900,
    "discountTotal": 0,
    "total": 4900,
    "lines": [
      {
        "seq": 1,
        "type": "BASE",
        "label": "Standard, monthly",
        "description": "1 × 49.00 EUR",
        "quantity": "1",
        "unitAmountDecimal": "4900",
        "amount": 4900,
        "serviceFrom": "2026-10-27T09:00:00Z",
        "serviceTo": "2026-11-27T09:00:00Z"
      }
    ]
  },
  "trialEndsAt": null,
  "trialRefusedReason": null,
  "requiresPaymentMethod": true
}

The token here is the one Cloudflare's always-pass test site key produces, which only the matching test secret accepts — a key set up for tests, as in the guide. A real page sends the token its widget gave it. Responses are illustrative.

Confirm

POST /public/checkout-sessions/{sessionId}/confirm, with Authorization: Bearer ksc_… and no body. Up to 24 hours may have passed since the start, so the confirm checks the order again — the price still public, the License still self-serve, the voucher, the trial — and answers one of three statuses:

statusWhenWhat the page does
completedNothing has to be captured: a FREE License, or a trial of a License that does not require a payment method. The Customer, the Instance and the subscription were just createdShows the outcome: session.customerSlug and session.instanceSlug are set
requires_payment_methodA payment method is needed: an amount is due today, or the License captures one before its trialSends the visitor to setupUrl, the payment provider's hosted page, before setupExpiresAt
awaiting_paymentBack from that page, the confirm created the Customer, the Instance and the subscription, and the first invoice is being chargedReads the session until it completes

The confirm is safe to send again: it answers the session as it stands, and never creates a second Customer, Instance, or charge. When a trial another checkout used up meanwhile is found before any payment method is captured, the session goes on without it: the confirm answers requires_payment_method with the new amount in session.dueToday, for the page to show before it sends the visitor on.

It is refused with 409 ConfirmCheckoutSession.SessionClosed for a session that is no longer open — its status in errors[0].value.status —, 409 .DueTodayChanged when what is due changed after the payment method was saved (nothing is charged: the visitor starts again), 409 .EntitlementLimitReached when your organization has reached its limit of customers or instances, 403 .AnonymousCheckoutDisabled when the key stopped opening the checkout since the start, and the 422 refusals of the start when the order no longer holds.

The payment provider's page

With Stripe, the page is a Stripe Checkout page in setup mode: Stripe creates its own customer with the visitor's e-mail, offers the payment methods turned on in your Stripe account, asks for the cardholder's name and billing address and offers a tax ID field, and charges nothing. It sends the visitor back to returnUrl with kaiten_checkout_session added; the page confirms again, and Kaiten reads the outcome of the setup from Stripe, never from the address.

A visitor who saves a payment method and never comes back loses nothing: Kaiten's sync with the provider sees the completed setup and completes the session itself, and so does the pass that closes sessions at their expiry. The same holds when the browser lost the session's token.

Completion

The completion runs in one transaction, recorded as Kaiten (system) — no one in your organization made it:

  • a Customer named after the e-mail, with that e-mail as its billing e-mail and a slug made from it — orders@izakaya-hanami.example becomes orders-izakaya-hanami-example-4f1c2a, the suffix random;
  • an Instance of that Customer, named after the e-mail as well, with a slug made the same way;
  • the subscription: for a paid License, charged automatically through your payment provider — the customer its page created when the visitor saved a payment method is linked to the new Customer; for a FREE License, with no payment provider at all.

Kaiten has no field for a company or a person's name: when your page asks for them, your backend renames the Customer after the checkout. Every checkout creates its own Customer — a buyer who checks out twice has two.

A paid License starts on its fallback

Nothing a visitor buys is granted before it is paid. A checkout with an amount due today creates the Instance on the default published version of the family's fallback — the free family its subscriptions fall back to when they end — with its subscription INCOMPLETE. When the first invoice, the activation, is paid, one transaction moves the Instance to the License bought, applies its add-ons and voucher, makes the subscription ACTIVE and the session COMPLETED, and emits checkout_session.v1.completed. Kaiten Sushi Shop's Standard falls back to Starter: a visitor who buys Standard runs on Starter while Stripe charges the card, then on Standard.

While it waits, session.payment.status says how the payment goes: processing, paid, requires_action — the visitor authorizes it, for 3-D Secure, on hostedInvoiceUrl — or failed, where the same page lets them pay with another card. An activation still unpaid after 23 hours ends the checkout: the invoice is voided at the provider, the subscription canceled, and the session EXPIRED, with checkout_session.v1.abandoned. The Customer and the Instance remain, on the fallback License, for you to delete.

A family with an amount due today therefore needs a fallback. Without one — or without a published default version in it — the start and the confirm answer 422 …FallbackRequired, and the Licenses list warns about a public family that has a paid version and no fallback. Set it in the family's License settings, under When a subscription ends, fall back to, or with fallbackLicenseFamilySlug on PATCH /license-families/{familySlug} (family settings): the fallback must be a family whose every version is FREE. Subscriptions covers what the fallback does when a subscription ends.

Trials and free Licenses

Two checkouts complete at the first confirm, with nothing to capture:

  • A trial without a card — a License with a trial and Require a payment method at sign-up off. Nothing is due today; the Instance runs on the License bought, its subscription in TRIAL, and what the trial becomes at its end without a payment method is the family's setting. A trial that does require a payment method goes through the provider's page first, and charges nothing.
  • A FREE License — no payment provider at all. The checkout needs no fallback and no Stripe; the subscription's invoices, of 0.00, are paid at issue.

A trial is given once per e-mail and License family: when a Customer of yours whose billing e-mail is the visitor's has already had a trial of the family, the checkout goes on without one, the first period is due today, and trialRefusedReason says ALREADY_CONSUMED.

Limits and lifetimes

Every start and every confirm counts against limits of its own, on top of the key's 20 requests a second:

Per visitor addressPer publishable key
Starts, dryRun included10 per 10 minutes and 30 per hour1,000 per hour
Confirms30 per 10 minutes1,000 per hour

The address is the visitor's, as the gateway reports it, and each API replica counts for itself. Past a limit the answer is 429 CreateCheckoutSession.RateLimited or 429 ConfirmCheckoutSession.RateLimited, with Retry-After. A self-hosted operator sets the four numbers with the KAITEN_BILLING_CHECKOUT_* variables.

  • A session stays open 24 hours (expiresAt). One still open after that reads ABANDONED, and the next lifecycle pass closes it with checkout_session.v1.abandoned — or completes it, when the visitor finished the provider's page.
  • Its ksc_ reads it until 24 hours after it closed, and then answers 401. It authenticates only from the starting key's origins, and stops with the key.
  • A closed session is deleted 90 days after it closed, with no event — KAITEN_BILLING_CHECKOUT_SESSION_RETENTION on a self-hosted installation.

What you see and receive

Customers › Checkouts

The Customers section of the console has a Checkouts tab wherever billing is enabled and your session holds read:billing. It lists the anonymous checkouts, newest first: their status — Open, Awaiting payment, Completed, Abandoned, Expired —, the e-mail with your reference under it, the License and its period, what is due today, when each was created and expires, the Customer and the Instance it created, and the publishable key whose page started it. Search and filters cover the e-mail, the reference, the status, and the period. An expired checkout says that its Customer and Instance remain.

The Checkouts tab of Customers: six checkouts of the Sushi Shop website key — open, awaiting payment, two completed, abandoned and expired — each with its e-mail and order reference, the License and period, the amount due today, its dates, and the customer and instance it createdThe Checkouts tab of Customers: six checkouts of the Sushi Shop website key — open, awaiting payment, two completed, abandoned and expired — each with its e-mail and order reference, the License and period, the amount due today, its dates, and the customer and instance it created

A row opens a panel with every member of the checkout: the lines of what was due, the trial or why it was refused, the Customer and the Instance created, and the moments of its life. Neither the list nor the panel ever shows the visitor's token or a voucher code — the API returns neither.

The panel of a completed checkout for orders@izakaya-hanami.example: Standard monthly, 49.00 EUR due today, and the customer and instance it createdThe panel of a completed checkout for orders@izakaya-hanami.example: Standard monthly, 49.00 EUR due today, and the customer and instance it created

Over the API, GET /checkout-sessions lists them — filtered by status (repeatable), email (in any letter case), and clientReferenceId (exactly), 50 to a page by default and up to 200 — and GET /checkout-sessions/{sessionId} reads one. Both need read:billing. The Customer and the Instance a checkout created say who made them — Created … by Kaiten (system) on their details cards — and the audit trail records the checkout's writes under the same actor.

Linking the buyer

Kaiten sends the buyer nothing — no welcome e-mail, no sign-in link. Your backend learns of the order from the checkout's events and does the rest: a sign-in to your product, your own e-mail, the Customer's name. The three events carry one payload:

MemberMeaning
sessionIdThe checkout session
emailThe address the visitor gave
clientReferenceIdYour reference, as the page gave it; null when it gave none
licenseSlug, billingPeriodWhat was bought
customerSlug, instanceSlugWhat the completion created; null before it
statusOPEN, COMPLETED, ABANDONED, or EXPIRED
EventEmitted when
checkout_session.v1.startedA checkout starts. A dryRun emits nothing
checkout_session.v1.completedIt completes: its first invoice is paid, or nothing was due. Provision on this event — never on INSTANCE_CREATED, which comes before the payment
checkout_session.v1.abandonedIt ends unpaid: ABANDONED, never completed and with no Customer, or EXPIRED, its Customer and Instance left for you to clean up

On Kaiten Cloud the events arrive as signed webhooks Kaiten Cloud; a self-hosted installation consumes them from its event stream. Link the buyer from the event — or from GET /checkout-sessions when an event was missed — never from what a browser reports: the outcome a page sees, the reference it holds, and the e-mail it typed are the visitor's to change.

Customer sessions

A customer you know — signed in to your product — acts through a customer session. Your backend mints it with a token holding write:customer_sessions, for one Customer and optionally one of its Instances, with a lifetime of 5 to 60 minutes, 30 by default; the token is shown once, and POST /customer-sessions/{sessionId}/revoke ends it sooner — on sign-out, or when the customer's access changes. A browser may use it only from an origin one of your live publishable keys allows. The call itself is on the authentication page.

RouteWhat it does
GET /public/session/portalThe subscription, the effective entitlements with what each layer contributes, the add-ons, the vouchers, the payment method, the trial, and what the customer may do
GET /public/session/invoicesThe customer's invoices, with how to pay each
POST /public/session/checkoutSubscribe the Instance to a price of the public catalogue, with add-ons and a voucher; dryRun answers what is due today
POST /public/session/billing/cancel, …/reactivateCancel at the end of the period paid for — a trial ends at once — and withdraw that before it ends
POST /public/session/billing/payment-method-session, …/{sessionId}/completeAdd or replace the payment method on the provider's page
POST /public/session/billing/portal-sessionOpen the payment provider's own billing portal
PUT /public/session/addons/{addonSlug}Attach an add-on, change its quantity, or remove it: an increase applies at once; on a live subscription, a decrease waits for the next renewal
POST /public/session/vouchers/validate, …/redeemCheck a voucher code; redeem one on a live subscription

A session bound to an Instance acts on that Instance's subscription; one bound to the Customer only reads the Customer, its Instances and its payment method, and a route that acts on an Instance answers 422 …InstanceRequired. What a session writes is recorded under the person or service account that minted it, and the events it causes say so in via: customer_session.

The session's checkout keeps the rules of the anonymous one: only a flat-fee price of a License in the public catalogue, every amount from the server, and with an amount due an INCOMPLETE subscription — the License bought applies once the first invoice is paid. An Instance on a free subscription can check out: the free subscription is replaced — once the first invoice is paid, when there is one to pay. Any other live subscription is refused with 409 CreateSessionCheckout.AlreadySubscribed — a change of License is yours to schedule. A trial is given once per Customer and License family.

Reference

OperationRouteCredential
Read the public catalogueGET /public/catalogPublishable key
Start an anonymous checkoutPOST /public/checkout-sessionsPublishable key
Read itGET /public/checkout-sessions/{sessionId}Its ksc_
Confirm itPOST /public/checkout-sessions/{sessionId}/confirmIts ksc_
List checkouts, read oneGET /checkout-sessions, GET /checkout-sessions/{sessionId}read:billing
Issue, list, change, revoke keysPOST /publishable-keys, GET, PATCH /publishable-keys/{keyId}, POST …/revokewrite:publishable_keys — read: to list
Mint and revoke customer sessionsPOST /customer-sessions, POST /customer-sessions/{sessionId}/revokewrite:customer_sessions
List a family, name its fallbackPATCH /license-families/{familySlug}write:licenses

The events of the checkout and of the keys are in the event catalog.

Where to go next

On this page