Skip to main content
This takes a plan_price_id, not a plan id - it identifies the specific billing variant (monthly vs. yearly, currency). Sending a plan id here fails with a validation error.
By default the session is created for the caller’s own identity (from their JWT) - the checkout flow your frontend triggers for the logged-in user, typically proxied through your backend using their own token (see withToken()). A secret-key (admin) client may instead pass { customer } to create the session for another customer; add { email } too when that customer has no Kerne user row (an externally-managed customer - Kerne needs an email to create one at the provider). The subscription itself is created asynchronously once the provider confirms payment - not synchronously when this call returns.

Where the redirect goes

Checkout success and cancel (and the portal return, see Portal & cancel) all resolve unconditionally to the hosted portal (apps/portal):
The portal then hands the session back to your own app: portal_redirect_url if you’ve set one, otherwise app_url. Configure either through the tenant configuration endpoint (not yet wrapped by @kerne/server - call it directly with your own token, same auth as the rest of this self-service API):
portal_redirect_url is optional - only set it if the portal should send the user somewhere other than app_url once a flow finishes.
Auth callbacks (verify email, reset password, magic link) resolve the same way - unconditionally to the portal, which then hands off to portal_redirect_url or app_url.

Headless (embedded card, Stripe only)

checkout() is the redirect path - the provider hosts the card form. For an embedded card form you build yourself, there are two server methods. They are Stripe-only, and there is no React prebuilt for this - @kerne/react’s useCheckout / <PricingTable> are redirect-oriented.
  1. createSetupIntent() - a Stripe SetupIntent for your own Stripe.js / Payment Element form. No redirect, no charge.
  2. Confirm clientSecret client-side with Stripe.js (stripe.confirmSetup(...) / stripe.confirmCardSetup(...)). That returns a Stripe payment method id.
  3. createSubscriptionHeadless() - attach that payment method and start the subscription, no browser involved.
    This is admin-gated (secret key). The payment method id is re-verified against Stripe server-side, so it can’t be used to skip payment collection - it requires one Stripe already confirmed belongs to this customer. Pass email when customer has no matching Kerne user row.
For CI or a seed script with no browser to confirm a SetupIntent, pass testCardToken (e.g. "tok_visa") instead of stripePaymentMethodId - it mints a fresh payment method server-side on each call. Stripe test mode only; rejected otherwise.