Skip to content

feat: customer sessions and self-serve checkout - #5

Draft
fuzcap wants to merge 1 commit into
feat/public-catalogfrom
feat/customer-sessions
Draft

fuzcap wants to merge 1 commit into
feat/public-catalogfrom
feat/customer-sessions

Conversation

@fuzcap

@fuzcap fuzcap commented Oct 7, 2026

Copy link
Copy Markdown

The SDK side of kaitencloud/kaiten#33: a vendor's backend mints a customer session, and the customer's browser checks out and reads its invoices with it.

Stacked on #4 (feat/public-catalog).

@kaitencloud/server

CustomerSessions.createCustomerSession and CustomerSessions.revokeCustomerSession, generated from the contract like the other resource classes (billing spec §14.6.2). The vendor backend calls it with its ksh_ token, which must hold write:customer_sessions:

const { data } = await CustomerSessions.createCustomerSession({
  client, body: { customerSlug: "acme", instanceSlug: "acme-prod" },
});
// hand data.token to the page; mint a new one before data.expiresAt

@kaitencloud/client

A session module, for a client created with authScheme: "bearer" and a tokenProvider that returns the session. The provider is called on every request, so the host can swap in a fresh token before expiresAt.

  • session.previewCheckout(order) → { status: "preview", dueToday, trialEndsAt }. Totals come from the server.
  • session.checkout(order) → one of:
    • requires_payment_method: send the customer to setupUrl, then call again with setupSessionId;
    • subscribed, with payment.status ∈ paid | requires_action | failed | processing | not_required.
  • session.invoices({ cursor?, limit? }) → the customer's issued invoices, with hostedInvoiceUrl. Poll it after requires_action or processing.
  • Shapes follow the SDK's conventions: currency → currencyCode, and a price reads as a PlanPrice.
  • With a publishable key, the session methods throw a TypeError before sending anything.

The checkout flow for the React side is §14.6.4: preview (dryRun) → requires_payment_method → hosted setup page (popup, redirect fallback) → second call with setupSessionId → subscribed / requires_action (link to hostedInvoiceUrl) / failed / processing (poll session.invoices()).

Contract snapshot

contracts/openapi.yaml is kaiten billing-step-11's, so most of this diff is generated: the billing surface of steps 2–11 in both packages. The public catalogue now maps the generated types instead of the hand-written ones from #4; its output is unchanged.

Tests

vp run -r build && vp check && vp test: 226 passing. New tests cover:

  • the session as bearer;
  • the checkout request body and the mapping;
  • preview and setup-page answers;
  • the invoice list;
  • the refusal under a publishable key, with no request sent;
  • the server's createCustomerSession.

@kaitencloud/server gains CustomerSessions.createCustomerSession and
revokeCustomerSession, generated like its other resource classes: a vendor's
backend mints a kst_ session for one of its customers and hands it to the
page. @kaitencloud/client gains a session module for that page -- checkout
preview, checkout and the customer's invoices -- over the bearer scheme it
already has; with a publishable key the session methods refuse before sending
anything.

The contract snapshot is kaiten billing-step-11's (kaitencloud/kaiten#33):
most of this diff is the generated billing surface. The public catalogue now
maps the generated types instead of hand-written ones; its output is
unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tom Ribuot <tom.ribuot@kaiten.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant