System-Kisum-Checkout (self-serve signup + Xendit payments)
Related documentation: Backend modules overview · Backend Auth · Backend Core · Data ownership · Kisum System.
Canonical implementation detail: repository System-Kisum-Checkout — start with README.md, then MEMORY.md for decisions, then AGENTS.md for boundaries.
Local dev host: checkout.local.kisum.io:3098. Production host: checkout.kisum.io.
Purpose
Section titled “Purpose”In one sentence:
System-Kisum-Checkoutis the payments BFF that owns the entire Kisum self-serve signup flow — Xendit Payment Sessions, the embedded Components card iframe, Recurring Plan creation, post-signup provisioning, webhook handling, AND post-trial card capture for existing customers via/billing/add-card— soBackend-Kisum-Authstays focused on identity andFrontend-Kisum-Websitestays a pure marketing site.
Platform role:
- Public face for
POST /api/checkout/session(paid signup),GET /create,POST /api/checkout/finalize,POST /api/checkout/signup(no-card trial),GET /billing/add-card+POST /api/billing/{session,finalize}(existing-customer card capture), and the Xendit webhook receivers. - Server-side holder of
XENDIT_SECRET_KEY,XENDIT_WEBHOOK_TOKEN,AUTH_INTERNAL_API_KEY,CORE_INTERNAL_API_KEY. None of these ever reach the browser. - Caller of the existing
Backend-Kisum-Auth/internal/admin/*andBackend-Kisum-Core/internal/*APIs. New endpoints added for this service:POST /internal/admin/users/{id}/sessions(mints tokens for a freshly created user) andGET /internal/admin/users/{id}/companies(membership rows used to gate/billing/add-card).
Trial vs paid path (2026-05-20 Trial Flags + Persona Pricing)
Section titled “Trial vs paid path (2026-05-20 Trial Flags + Persona Pricing)”The trial decision is data-driven from Core, not a build flag:
packages.trial_enabled | Marketing site posts to | What Checkout does |
|---|---|---|
true | POST /api/checkout/signup | Provisions status='trial' rows for Basic + every cart addon. No Xendit call. Hands the user tokens directly. |
false | POST /api/checkout/session | Opens a Xendit Payment Session (mode=COMPONENTS, card_on_file_type=RECURRING + recurring_configuration). User completes the card iframe on /create; /api/checkout/finalize creates the Customer + Recurring Plan and provisions status='active' rows. |
Hard guard: /api/checkout/signup refuses non-trial packages; /api/checkout/session refuses trial packages. The marketing site decides which to call by fetching the chosen package from Core’s /public/packages?key=… and reading trialEnabled client-side.
Persona handoff
Section titled “Persona handoff”The signup bundle includes audience: 'promoter' | 'venue'. After provisioning, the post-signup productLoginUrl is resolved per-persona from PRODUCT_LOGIN_URL_PROMOTER (Frontend-Kisum) or PRODUCT_LOGIN_URL_VENUE (Frontend-Kisum-Venues).
Post-trial card capture (/billing/add-card)
Section titled “Post-trial card capture (/billing/add-card)”When a trial company wants to keep the subscription alive past ends_at, the product app links the user to:
https://checkout.kisum.io/billing/add-card?companyId=<uuid>&returnTo=<absolute-url>Checkout:
- JWKS-verifies the shared
kisum_access_tokencookie (.kisum.io) via Auth’s/.well-known/jwks.json. - Confirms the user has an active membership on
companyIdvia Auth’s newGET /internal/admin/users/{id}/companies. - Opens a Xendit Payment Session anchored to today + 1 month for the recurring config.
- On
session-complete, creates a Xendit Customer + Recurring Plan matching the company’sgetCompanyEntitlements()set and flips Basic + addons fromtrialtoactivein Core. - Redirects back to the product app’s
returnTo.
Product apps never embed Xendit Components themselves. See Frontend-Kisum/src/components/billing/BillingAddCardLink.tsx for the canonical entry-point component.
Existing-company add-on purchase (/billing/upgrade)
Section titled “Existing-company add-on purchase (/billing/upgrade)”Since 2026-06-09, product apps (starting with Frontend-Kisum-Promoters /profile/billing) can add add-ons to an existing company:
https://checkout.kisum.io/billing/upgrade?companyId=<uuid>&addonKey=<key>&returnTo=<absolute-url>| Route | Purpose |
|---|---|
POST /api/billing/upgrade/session | Auth + validate add-on; $0 add-ons → Core POST /internal/companies/{id}/addons immediately (mode: free); paid → Xendit Payment Session (mode: paid) |
POST /api/billing/upgrade/finalize | After Xendit session-complete: provision add-on in Core + rebuild Recurring Plan to match entitlements |
POST /api/billing/upgrade/cancel | Cancel active add-on (status='cancelled' in Core); CORS-enabled for product-app origins (PRODUCT_LOGIN_URL*) |
Cancel is invoked cross-origin from the product app with credentials: 'include' (shared kisum_access_token on .kisum.io). Add uses a full navigation deep-link (no CORS).
This page records cross-service positioning. The repository is the living reference for routes, payload shapes, and env.
Mental model
Section titled “Mental model”Marketing /create (step 1: account + company form) ↓ POST /api/checkout/sessionCheckout (Next.js) ↓ Xendit POST /sessions (mode=COMPONENTS, RECURRING card-on-file) ↑ componentsSdkKey + paymentSessionId ↓ Redis: stash signup body under `checkout:pending:{referenceId}` (TTL ~30 min) ↑ { paymentSessionId, componentsSdkKey, referenceId, redirectUrl }
Browser redirects to https://checkout.kisum.io/create?referenceId=… ↓ mount `xendit-components-web` against componentsSdkKey ↓ user enters card in Xendit-hosted iframe → SDK fires `session-complete`
POST /api/checkout/finalize { referenceId } ↓ Xendit GET /sessions/:id (assert completed, extract payment_token_id) ↓ Xendit POST /customer + POST /recurring/plans (anchor_date=now+TRIAL_DAYS, immediate_payment=false) ↓ Auth POST /internal/admin/users ↓ Core POST /internal/companies + /basic + /addons (status=trial) ↓ Auth POST /internal/admin/companies/{id}/memberships (TENANT_SUPERADMIN) ↓ Auth PUT /internal/admin/company-memberships/{mid}/permission-grants ↓ Auth POST /internal/admin/users/{id}/sessions (mint tokens) ↑ { accessToken, refreshToken, productLoginUrl } ↓ browser redirects to PRODUCT_LOGIN_URL?email=…The marketing site only knows checkout.kisum.io. Auth and Core are reached only server-to-server from Checkout.
Sequence
Section titled “Sequence”sequenceDiagram
participant User
participant Web as marketing
participant CK as System_Kisum_Checkout
participant XD as Xendit
participant Auth as Backend_Kisum_Auth
participant Core as Backend_Kisum_Core
User->>Web: Step 1 account + company form
Web->>CK: POST /api/checkout/session
CK->>CK: Persist signup draft in Redis
CK->>XD: POST /sessions mode=COMPONENTS
XD-->>CK: paymentSessionId + componentsSdkKey
CK-->>Web: redirectUrl
Web-->>User: window.location = checkout.kisum.io/create?referenceId=...
User->>CK: GET /create?referenceId=...
CK->>CK: Load draft + GET session for fresh sdkKey
User->>CK: submit card via Xendit iframe
XD-->>CK: session-complete event
CK->>XD: GET /sessions/:id (verify + extract payment_token_id)
CK->>XD: POST /customer + POST /recurring/plans
CK->>Auth: POST /internal/admin/users
CK->>Core: POST /internal/companies + basic + addons
CK->>Auth: POST /internal/admin/companies/.../memberships
CK->>Auth: PUT /internal/admin/company-memberships/.../permission-grants
CK->>Auth: POST /internal/admin/users/.../sessions
Auth-->>CK: { accessToken, refreshToken }
CK-->>User: redirect to PRODUCT_LOGIN_URL?email=...
XD-->>CK: POST /api/webhooks/xendit/recurring (cycle.succeeded)
CK->>Core: upsert basic + addons (status=active)
Endpoints
Section titled “Endpoints”| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/checkout/session | none (CORS-restricted to MARKETING_ORIGIN) | Step 1. Validates account body, opens Xendit Payment Session, stashes draft in Redis, returns SDK key + redirect URL. |
| GET | /api/checkout/session/[referenceId] | none | Re-hydrate draft on /create refresh / 3DS return. |
| POST | /api/checkout/finalize | none (idempotent by referenceId) | Step 2. Verifies session, extracts payment_token_id, runs the full provisioning pipeline, returns tokens. |
| POST | /api/checkout/signup | none (CORS-restricted) | No-card free-trial path (VITE_TRIAL_CARD=false on marketing). Provisions Basic + every catalog add-on as trial. |
| GET | /api/geo/countries | none (CORS-restricted) | Country dropdown on marketing /create. Proxies Backend-Kisum-Artists via ARTISTS_INTERNAL_*. |
| GET | /api/geo/countries/:code/cities | none (CORS-restricted) | City lookup for signup form (same Artists upstream). |
| POST | /api/webhooks/xendit/recurring | x-callback-token | Recurring lifecycle. cycle.succeeded → Core upsert active; terminated/inactive/stopped → Core upsert cancelled. |
| POST | /api/webhooks/xendit/payments | x-callback-token | Idempotent ack-only safety net for session events. Provisioning still runs via the browser-initiated finalize today. |
Data ownership
Section titled “Data ownership”Checkout is intentionally stateless except for Redis drafts. It does not own any platform master data.
| Data | Owner | Where |
|---|---|---|
| Signup draft between session + finalize | Checkout | Redis key checkout:pending:{referenceId} (TTL XENDIT_CHECKOUT_SESSION_TTL_MIN, default 30 min) |
| Xendit Customer / Recurring Plan | Xendit (external) | Referenced by id; Checkout stores ids in Core company metadata.xendit |
| Users + sessions + memberships + permission grants | Auth | Auth DB — see Data ownership |
| Companies + commercial state + subscriptions + add-ons | Core | Core DB |
| Pricing table | Checkout | src/lib/pricing.ts (server-side; marketing site mirrors for display only) |
Security boundary
Section titled “Security boundary”- Secrets stay server-side:
XENDIT_SECRET_KEY,XENDIT_WEBHOOK_TOKEN,AUTH_INTERNAL_API_KEY,CORE_INTERNAL_API_KEYlive inSystem-Kisum-Checkout/.env.localonly. The browser never sees them. NoNEXT_PUBLIC_*aliases. - HTTPS required: Xendit Components rejects plain-HTTP origins. Local dev tunnels port 3098 through ngrok and adds the tunnel host to
XENDIT_COMPONENTS_ORIGINS. - CORS: Cross-origin browser calls are allow-listed by origin CSV env vars (see below). Middleware:
src/middleware.ts+src/lib/cors.ts(MARKETING_ORIGIN) andsrc/lib/billing-cors.ts(PRODUCT_ORIGINS). Code default when unset:src/lib/cors-origins.ts.MARKETING_ORIGIN—/api/checkout/*,/api/geo/*(marketing signup + country dropdown).PRODUCT_ORIGINS—POST /api/billing/upgrade/cancel(module apps with credentials).- Production CSV:
https://www.kisum.io,https://admin.kisum.io,https://app.kisum.io,https://kisum.io,https://artists.kisum.io,https://venues.kisum.io. - Local dev: append
http://local.kisum.io:5173,http://app.local.kisum.io:3000, etc. in.envonly — not in Worker secrets.
- Geo reference:
GET /api/geo/*proxies Backend-Kisum-Artists (ARTISTS_INTERNAL_BASE_URL=https://api-v2-artists.kisum.dev,ARTISTS_INTERNAL_API_KEY). Do not use deprecatedMARKET_INTERNAL_*names. - Webhook token: verified with
crypto.timingSafeEqualagainstXENDIT_WEBHOOK_TOKEN. Length mismatch → false without leaking timing. - Pricing trust: the marketing site can POST any module names; only matches in the server-side
pricing.tstable are charged.
Local dev
Section titled “Local dev”# /etc/hosts (one-time)127.0.0.1 checkout.local.kisum.io
# 1. Start Auth (:3097), Core (:3096), Redis as usual.cd Backend-Kisum-Auth && go run ./cmd/apicd Backend-Kisum-Core && go run ./cmd/api
# 2. Checkoutcd System-Kisum-Checkoutcp .env.example .env.local # fill XENDIT_*, AUTH_INTERNAL_API_KEY, CORE_INTERNAL_API_KEYnpm installnpm run dev # 0.0.0.0:3098
# 3. HTTPS tunnel for Xendit Components (port 3098, not the marketing 5173)ngrok http http://checkout.local.kisum.io:3098 \ --host-header=checkout.local.kisum.io \ --domain=enjoyably-snipping-tackling.ngrok-free.dev
# 4. Marketing sitecd Frontend-Kisum-Website# .env.local: VITE_CHECKOUT_BASE_URL=http://checkout.local.kisum.io:3098npm run dev # 0.0.0.0:5173
# 5. Xendit dashboard — update webhook URLs:# POST https://<ngrok>/api/webhooks/xendit/recurring# POST https://<ngrok>/api/webhooks/xendit/payments# and add the ngrok hostname to the Domains allow-list.What lives elsewhere (do not duplicate)
Section titled “What lives elsewhere (do not duplicate)”- Identity / sessions / JWT / JWKS / password reset / login / refresh / logout —
Backend-Kisum-Auth. - Company master data, package + module catalog, subscription truth —
Backend-Kisum-Core. - Marketing landing pages, cart sidebar UX, pricing display copy —
Frontend-Kisum-Website. - Product app surface (post-login) —
Frontend-Kisum. - Webhook handling for any non-Xendit payment provider — none today; would go here if added.
Breaking change (2026-05-20)
Section titled “Breaking change (2026-05-20)”Backend-Kisum-Auth no longer exposes POST /auth/signup, POST /auth/checkout, POST /auth/checkout/session, or any /webhooks/xendit/* routes. Callers were the marketing site only; it now POSTs to System-Kisum-Checkout endpoints listed above. Auth gained one new internal endpoint — POST /internal/admin/users/{id}/sessions — used only by Checkout to mint tokens for a freshly-provisioned user.