Guides
Sell a License Online, with No Account
The whole path on Kaiten Sushi Shop — Standard priced with Starter as its fallback, Stripe in test mode, a publishable key behind Cloudflare Turnstile, the React page, a visitor's purchase, what the vendor sees, and the backend that welcomes the buyer.
Kaiten Sushi Shop sells Standard to restaurants it has never heard of. A visitor opens its pricing page, picks Standard, types the restaurant's name and an e-mail, passes a security check, and gives a card on Stripe's page — and Kaiten holds a Customer, an Instance and a paid subscription for them, with no account created anywhere beforehand. This guide builds that path end to end: the catalogue, the payment provider, the publishable key, the page, the purchase, what the vendor sees, and the backend that welcomes the buyer. Self-serve checkout explains each mechanism the steps rely on.
The recording shows that path from the buyer's side, in Stripe's test mode, with the vendor's console in between — then what the customer portal adds once the restaurant is a customer: its limits, an add-on it buys itself, and its invoices.
You need billing enabled for your organization and a Stripe account in test mode. The console steps need a session that may write Licenses, connectors and publishable keys and read billing; their API equivalents need $KAITEN_API and a token in $KAITEN_TOKEN holding write:licenses, write:organizations, write:publishable_keys and read:billing, set up as in the quickstart. The page is a React app.
With the demo seed profile — the Community Edition's task dev — the first step is already done: the seed writes Kaiten Sushi Shop's catalog, Starter free, Standard at 49.00 EUR a month or 490.00 EUR a year, the families public and Standard falling back to Starter. Read it in the console and go on to Stripe. The Kaiten Cloud demo sandbox does not write that catalog yet: there, as in an organization of your own, the first step creates it. Either way the seed sells nothing — the payment provider, the publishable key and every purchase are yours to add.
Price Standard, and make Starter its fallback
Starter is free and Standard is paid; the public catalogue lists both, and Standard falls back to Starter — which is what lets a visitor buy it with no account: until the first payment, their Instance runs on Starter.
- Open Licenses, then Starter's version. On the Commercial terms card, Edit: Pricing type
Free, then Save. On the Prices tab, Add price: ShapeFlat fee, Billing timingIn advance, Billing periodMonthly, CurrencyEUR, Label on the invoiceStarter, Amount0.00, Default price of this period checked, then Create price. A License is bought online only with a default flat-fee price, and 0.00 is one. - Open Standard v1, the version the family serves. Commercial terms: Pricing type
Paid, no trial. Prices:Standard, monthlyat 49.00 EUR, Monthly, andStandard, yearlyat 490.00 EUR, Annual — each the default price of its period. The first price fixes the version's currency. The third price on the capture below, 0.50 EUR per order above the allowance, is optional: it is billed in arrears, never at a checkout. - Back on Licenses, open Standard's Settings. In License settings, When a subscription ends, fall back to
Starter— only families whose every version is free are offered — and leave the trial settings as they are. Save settings. - On the Licenses list, switch List Starter in the public catalog and List Standard in the public catalog on. Premium can be listed as well: with Pricing type
Customand a Call-to-action URL to your sales page on its default version, the pricing table offers a link to that page instead of a checkout.
Connect Stripe in test mode
Standard is paid, so it needs a payment provider that captures cards. Until one is connected, the catalogue says Standard is not self-serve, and a checkout of it is refused with ProviderCannotCapturePayment.
Open Integrations › Connectors › Stripe. Paste a restricted key of your Stripe account in test mode (rk_test_…) in Restricted API key, keep the default settings, and Connect Stripe. The page then reads Connected and Test mode; the key is stored in Vault and never shown again. Stripe lists the permissions the key needs and what each setting does.
Create a publishable key that opens the anonymous checkout
The page reads the catalogue and starts checkouts with a publishable key. Open Integrations › Publishable keys › New publishable key:
- Label
Sushi Shop website. Allowed origins, one per line:https://kaiten-sushi.exampleandhttps://www.kaiten-sushi.example— your shop's own origins, exactly as a browser sends them, with no path. - In Anonymous checkout, the Turnstile site key and the Turnstile secret of a Cloudflare Turnstile widget registered for those hostnames. Then switch Allow anonymous checkout on: it stays off until both values are filled.
- Create key, then Copy the key: it is shown once. The list then shows the key by its last four characters, with the Live and Anonymous checkout badges.
For a test on your own machine, Cloudflare publishes test keys that need no Cloudflare account: the site key 1x00000000000000000000AA always passes, and the secret 1x0000000000000000000000000000000AA accepts any token. With that secret, Cloudflare reports every widget as solved on example.com, whatever page it ran on — and Kaiten refuses a start whose reported host is not one of the key's origins. A key that carries the test pair therefore also lists https://example.com, beside your local origin: http://localhost:5173 for the storefront example. Keep such a key for tests only: with a secret that accepts any token, the Turnstile check stops no one.
Put the pricing table and the checkout on your page
The React SDK draws both. Install the browser client and the components — the headless core, @kaitencloud/react, comes with them:
npm install @kaitencloud/client @kaitencloud/react-uiAt the root of the shop, one provider in public mode reads the catalogue with the key, and one anonymous checkout client — made once, outside render, since it keeps the token of the checkout it starts — lets the components start checkouts:
import { KaitenAnonymousCheckoutClient } from "@kaitencloud/client";
import { KaitenProvider, KaitenStyleRoot } from "@kaitencloud/react-ui";
import "@kaitencloud/react-ui/styles.css";
const apiUrl = "https://kaiten.example.com/api"; // your Kaiten API, /api included
const publishableKey = "pk_…"; // the Sushi Shop website key
const anonymousCheckoutClient = new KaitenAnonymousCheckoutClient({ apiUrl, publishableKey });
export function Shop() {
return (
<KaitenProvider
mode="public"
apiUrl={apiUrl}
publishableKey={publishableKey}
anonymousCheckoutClient={anonymousCheckoutClient}
>
<KaitenStyleRoot>{/* your routes: the pricing page and the checkout page */}</KaitenStyleRoot>
</KaitenProvider>
);
}The pricing page is one component. A card's button hands over the License and the price it showed; the checkout gets an address of its own, because Stripe's page sends the visitor back to the page that sent them:
import { PricingTable } from "@kaitencloud/react-ui";
export function PricingPage() {
return (
<PricingTable
onSelectPlan={(slug, _plan, price) =>
navigate(`/checkout/${slug}${price?.id ? `?price=${price.id}` : ""}`) // your router
}
/>
);
}The checkout page mounts AnonymousCheckoutFlow on the License the card named:
import { AnonymousCheckoutFlow, usePlans } from "@kaitencloud/react-ui";
export function CheckoutPage({ slug, priceId, orderId }: { slug: string; priceId?: string; orderId: string }) {
const { data: plans } = usePlans();
const plan = plans?.find((each) => each.slug === slug);
if (!plan) return null;
return (
<AnonymousCheckoutFlow
plan={plan}
priceId={priceId}
clientReferenceId={orderId} // a reference of yours, echoed on the checkout's events
collectCompany="required" // the restaurant's name: your page's field, never sent to Kaiten
onSubscribed={(outcome) => showThankYou(outcome.session.email, outcome.company)}
/>
);
}- Mount the flow on the page the visitor comes back to. Its
returnUrlis the current page by default; back from Stripe's page — a popup, or the whole page when the browser blocks popups — the flow confirms the checkout again and waits for the payment. - The company is yours. Kaiten has no field for a company or a person's name:
collectCompanyandcollectNameadd the fields to your page and hand what was typed toonStartedandonSubscribed, and to nothing else. Step 7 names the Customer after it. onSubscribedis for the screen. What a browser reports — the outcome, the reference, the e-mail — can be changed by the visitor: your backend acts on the signed event of step 7, never on this callback.- Allow Cloudflare. A page with a
Content-Security-Policyallowshttps://challenges.cloudflare.cominscript-src,frame-srcandconnect-src, or the security check never appears.
The storefront example in the public sdk-js repository is this page, complete: the pricing table, the checkout for visitors and for signed-in customers, the customer portal, and the backend route that names the Customer. It runs against a stand-in for Kaiten until you give it yours. Checkout covers every prop of the flow.
Buy Standard as a visitor
Open the pricing page in a new tab. Starter and Standard carry their prices — the monthly and annual toggle switches Standard between 49.00 and 490.00 EUR — and, if you listed it, Premium links to your sales page. Choose Standard:
- The checkout page asks for the restaurant's name — your page's field — and an e-mail:
orders@izakaya-hanami.example. - The security check draws Cloudflare's widget. With the test site key, it passes by itself.
- Continue starts the checkout. The review shows what Kaiten says is due today, 49.00 EUR, and when Standard renews.
- Continue to payment opens Stripe's page. Enter the test card
4242 4242 4242 4242, any future expiry date, any CVC, a cardholder name and a billing address. Nothing is charged on that page: it saves the card. - Back on your page, the flow confirms the checkout, waits for the first payment, and says the order is registered for the address.
Starter's checkout is shorter: after the e-mail and the security check, one button — nothing to pay, no Stripe page — and it completes at once.
See what the purchase created
Open Customers › Checkouts. The checkout reads Completed, with the e-mail, your reference, Standard monthly, 49.00 EUR due today, and links to the Customer and the Instance it created. A checkout still waiting for its payment reads Awaiting payment; one left open for a day, Abandoned.
Open the Customer. Its name and its billing e-mail are the address the visitor typed, and its details say Created … by Kaiten (system): no one in your organization created it. Its Instance ran on Starter until Stripe confirmed the payment, and runs on Standard since. Its Billing tab shows the subscription — Standard, monthly, charged automatically through Stripe —, the upcoming invoice, and the invoices, the first of them paid. Every subscribed Instance's Billing tab reads the same way; here, Sakura Tokyo's production instance, on Premium v2 yearly with two packs of extra delivery drivers:
The first invoice is the activation: Kaiten composed it — the reference for its lines and amounts — and Stripe numbered it and collected the payment. Demo Restaurant's, subscribed to Standard monthly with the WELCOME20 voucher, reads like this:
Welcome the buyer from your backend
Kaiten sends the buyer nothing: no welcome e-mail, no sign-in link. Your backend learns of the order from checkout_session.v1.completed — the e-mail, your clientReferenceId, the License and the slugs of the Customer and the Instance — and does what a new customer needs: a sign-in to your product, your own e-mail, the Customer named after the restaurant.
On Kaiten Cloud, subscribe an endpoint to the event — Integrations › Webhooks, group Checkouts — and verify each delivery's signature Kaiten Cloud:
import express from "express";
import { verifyKaitenWebhook } from "@kaitencloud/server";
const app = express();
app.post("/webhooks/kaiten", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = await verifyKaitenWebhook({
payload: req.body, // the raw bytes, as signed
headers: req.headers,
secret: process.env.KAITEN_WEBHOOK_SECRET!, // the endpoint's whsec_…
});
} catch {
return res.sendStatus(401);
}
switch (event.type) {
case "com.kaiten.checkout_session.v1.completed": {
const { sessionId, email, clientReferenceId, customerSlug, instanceSlug } = event.data;
// A delivery can come twice: create once per checkout session.
await accounts.createOnce(sessionId, { email, clientReferenceId, customerSlug, instanceSlug });
await mailer.sendWelcome(email);
break;
}
case "com.kaiten.checkout_session.v1.abandoned":
// EXPIRED: unpaid; its Customer and Instance remain, on Starter, for you to delete.
break;
default:
break;
}
res.sendStatus(204);
});accounts and mailer stand for your own code. The signature covers the raw bytes, so the route takes the body unparsed.
To name the Customer after the restaurant, your backend needs what only the page knows: the page sends the company from onSubscribed, and your backend checks the Customer it names before updating it — created by Kaiten (system), its billing e-mail the one the page reports, still named after that address. PUT /customers/{customerSlug} replaces the record: send back the fields you read. The storefront example does exactly this.
A self-hosted installation has no managed webhooks: consume CHECKOUT_SESSION_COMPLETED from its event stream instead. Either way, GET /checkout-sessions?status=COMPLETED lists the checkouts an endpoint or a consumer missed.
What you built
A visitor bought Standard from your page with an e-mail, a security check and a card: Kaiten priced the order, Stripe took the card on its own page, the Instance ran on Starter until the payment and on Standard after it, and your backend heard of the order from a signed event. Nothing in your page held a secret: the publishable key ships in its source, and the checkout's own token never leaves the visitor's tab.
Where to go next
Self-serve checkout
The three public credentials, every refusal of the start and the confirm, trials and free Licenses, the limits, and the customer session for signed-in customers.
Checkout components
Every prop of AnonymousCheckoutFlow and CheckoutFlow, and what happens when a browser loses the checkout.
Subscriptions
What follows the purchase: renewals, trials, cancellation, and the fallback when a subscription ends.
Meter Usage and Enforce Limits
Report consumption against NUMBER grants on three seeded instances — a hard cap that refuses the next report, an allowance that accepts a report past the cap, an unlimited grant — then correct a counter, wire the events, and design a quota of your own.
Progressive Rollouts
Ship a feature in stages with one flag — a single customer first, then a deterministic percentage, a dated ramp, general availability, and the kill switch — with rules that read the contract or the caller's context.