Kaiten

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.

  1. Open Licenses, then Starter's version. On the Commercial terms card, Edit: Pricing type Free, then Save. On the Prices tab, Add price: Shape Flat fee, Billing timing In advance, Billing period Monthly, Currency EUR, Label on the invoice Starter, Amount 0.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.
  2. Open Standard v1, the version the family serves. Commercial terms: Pricing type Paid, no trial. Prices: Standard, monthly at 49.00 EUR, Monthly, and Standard, yearly at 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.
  3. 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.
  4. 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 Custom and 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.
The Prices tab of Standard v1: Standard, monthly at 49.00 EUR and Standard, yearly at 490.00 EUR, each the default of its period, and Orders above the allowance at 0.50 EUR per order, billing above 100 orders a month, up to 110The Prices tab of Standard v1: Standard, monthly at 49.00 EUR and Standard, yearly at 490.00 EUR, each the default of its period, and Orders above the allowance at 0.50 EUR per order, billing above 100 orders a month, up to 110The License settings dialog of Standard: when a subscription ends, fall back to Starter; a trial that ends without a payment method is canceled; the trial reminder three days before the endThe License settings dialog of Standard: when a subscription ends, fall back to Starter; a trial that ends without a payment method is canceled; the trial reminder three days before the end

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.

The Stripe connector page: the Connected and Test mode badges, the connector's settings, who does what between Kaiten and Stripe, and the permissions of the restricted keyThe Stripe connector page: the Connected and Test mode badges, the connector's settings, who does what between Kaiten and Stripe, and the permissions of the restricted key

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:

  1. Label Sushi Shop website. Allowed origins, one per line: https://kaiten-sushi.example and https://www.kaiten-sushi.example — your shop's own origins, exactly as a browser sends them, with no path.
  2. 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.
  3. 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.
The New publishable key dialog: label Sushi Shop website, the two allowed origins, and the Anonymous checkout section switched on, with a Turnstile site key and the Turnstile secret maskedThe New publishable key dialog: label Sushi Shop website, the two allowed origins, and the Anonymous checkout section switched on, with a Turnstile site key and the Turnstile secret masked

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-ui

At 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 returnUrl is 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: collectCompany and collectName add the fields to your page and hand what was typed to onStarted and onSubscribed, and to nothing else. Step 7 names the Customer after it.
  • onSubscribed is 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-Policy allows https://challenges.cloudflare.com in script-src, frame-src and connect-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:

  1. The checkout page asks for the restaurant's name — your page's field — and an e-mail: orders@izakaya-hanami.example.
  2. The security check draws Cloudflare's widget. With the test site key, it passes by itself.
  3. Continue starts the checkout. The review shows what Kaiten says is due today, 49.00 EUR, and when Standard renews.
  4. 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.
  5. 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.

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 createdThe panel of the completed checkout of orders@izakaya-hanami.example: Standard monthly, 49.00 EUR due today, and the customer and instance it createdThe panel of the completed checkout of orders@izakaya-hanami.example: Standard monthly, 49.00 EUR due today, and the customer and instance it created

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 Billing tab of sakura-tokyo-prod: its subscription to Premium v2 yearly through Stripe, the upcoming invoice, its two packs of Extra delivery drivers, and its invoicesThe Billing tab of sakura-tokyo-prod: its subscription to Premium v2 yearly through Stripe, the upcoming invoice, its two packs of Extra delivery drivers, and its invoices

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:

The activation invoice of Demo Restaurant, paid: its Stripe card with the reconciliation Match, and its lines — Standard, monthly at 49.00 EUR and the Welcome offer discount of 9.80 EUR — for 39.20 EURThe activation invoice of Demo Restaurant, paid: its Stripe card with the reconciliation Match, and its lines — Standard, monthly at 49.00 EUR and the Welcome offer discount of 9.80 EUR — for 39.20 EUR

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

On this page