Skip to content
Indiana
Dashboard
Docs
Browse docs / Memberships & billing

Memberships & billing

Purchase, change, cancel and recover subscriptions directly or through a Funnel.

SDK 0.0.1Updated

Preview, confirm and recover a purchase

Use npm @tinkerlandai/indiana-sdk@0.0.1 with a matching server. Save current feature definitions and publish a membership product, then configure this App’s Stripe Sandbox connection and price mappings. Grant billing:write, plus entitlements:read for holdings. This trusted-backend fragment uses groove from the quickstart’s lib/groove-server.ts. Map customerId from your verified login session.

previewAttach saves a 15-minute quote without payment. A Checkout URL from attach does not prove payment; complete means the operation completed, and readHoldings confirms access. Retain operationId after a lost response rather than creating another order. Use a new requestId when an unaccepted quote expires or the selection changes. Browser methods omit customerId.

For discounts, call listOffers and pass the held offer’s id as previewAttach.offerId, not the Coupon definition ID. Initial Stripe Checkout accepts one coupon; changing it requires a new quote. Amounts use integer currency minor units; the provider confirms automatic tax. Multiple plans must satisfy supported common connection, currency and billing-period conditions. Native purchases use fixed SKUs and store verification.

server/subscription-purchase.ts · fragmentTypeScript
import { groove } from "@/lib/groove-server"; // Persist this complete input before requesting a quote; reuse it on retry.const input = {  customerId: verifiedCustomer.id,  requestId: retainedRequestId,  plans: [{ productVersionId: selectedPublishedVersionId }],  provider: "stripe" as const,  currency: "USD" as const,};const { quote } = await groove.previewAttach(input);// Save quote.operationId. Show the quote and obtain explicit customer acceptance.// Execute only after that acceptance, using the same verified customer.const { operation } = await groove.attach({  customerId: verifiedCustomer.id,  operationId: quote.operationId,});// Return operation.checkoutUrl to the browser when present.// After payment return or a lost response, recover this original operation:const recovered = await groove.readSubscriptionOperation({  customerId: verifiedCustomer.id,  operationId: quote.operationId,});

Plan changes, seats and cancellation

Stripe uses immediate or renewal, targeting one membership while retaining sibling plans. pending awaits payment or recovery, scheduled records a future change, and effective means the change took effect. Verified payment determines paid fulfillment. Seats use change: { type: 'seats', licenseKey, quantity }; prepaid feature quantities use { type: 'quantity', entitlementVersionId, quantity }, with decreases at renewal. cancel/uncancel control renewal; cancellation and refunds are separate operations.

Native plan replacements use timing: store with host store instructions and verified transactions. Stripe proration or variable seats do not apply to Apple/Google. A published interface does not certify your deployment, SKUs, trial eligibility or real payments.

server/subscription-update.ts · fragmentTypeScript
const { quote } = await groove.previewUpdate({  customerId: verifiedCustomer.id,  requestId: retainedChangeRequestId,  membershipId: currentMembershipId,  timing: "immediate",  change: { type: "plan", productVersionId: targetPublishedVersionId },});// Persist the quote, show its amounts and timing, and obtain customer acceptance.await groove.update({  customerId: verifiedCustomer.id,  operationId: quote.operationId,});const { operation } = await groove.readSubscriptionUpdate({  customerId: verifiedCustomer.id,  operationId: quote.operationId,});

Payment and membership actions

Publish a product version and configure its explicit Sandbox provider binding. Funnel checkout and action nodes perform purchase, trials, membership changes, cancellation and resume. Cost confirmation shows the actual credit, debit, discount, tax, total and effective time.

Pending payment remains waiting. A redirect does not prove payment. Read current holdings after server verification; real provider acceptance requires separate evidence.
app/billing.tsTypeScript
import { createFunnelRunController, showFunnelRun } from "@tinkerlandai/indiana-sdk"; // Your backend retains the original start request and run credential.export async function openFunnel(container: HTMLElement) {const response = await fetch("/api/groove-funnel", {  method: "POST", credentials: "same-origin", cache: "no-store",});if (!response.ok) throw new Error("Funnel bootstrap failed");const { runToken } = await response.json();const client = createFunnelRunController({  baseUrl: "https://grow.your-domain.example",  scope: { projectId: "your-app-uuid", environment: "sandbox" },  runToken,});const handle = await showFunnelRun(container, {  client,  onAuthenticate: async () => obtainCurrentUserCustomerToken(),  onCheckout: ({ url }) => window.location.assign(url),  onOpenUrl: ({ url }) => window.location.assign(url),  onError: reportError,});// Keep the host's original bootstrap across navigation and payment returns.// Destroy removes UI; close is an explicit server command.return { handle, client, destroy() { handle.destroy(); client.reset(); } };}// On route leave: flow.destroy(). On explicit dismissal: await flow.handle.close().