Payments
Use managed payments when you want Kizaki to own checkout, subscription state, and Stripe synchronization instead of rebuilding those paths yourself.
Payments are worth treating as infrastructure, not as a handful of helper functions.
If your app sells plans, trials, or seats, the dangerous part is not deciding which feature belongs to Pro. The dangerous part is everything around it: mapping customers to subscriptions, keeping local state in sync with Stripe, handling failures, and making sure your product can reason about payment state without every workflow re-implementing billing logic.
That is the job of managed payments.
When To Use The Managed Surface
Use the payments block when you want Kizaki to own:
- plan-to-Stripe price resolution
- checkout session creation
- subscription state stored locally by the platform
principal.planandprincipal.planStatus- Stripe webhook verification and deduplication
- payment lifecycle effects
- local payment simulation during development
If your billing model is unusual or Stripe-specific enough that the managed surface is too narrow, you can still use raw Stripe directly. The important part is making that a deliberate choice rather than drifting into it accidentally.
The Recommended Setup
For most SaaS-style apps, the clean path is:
- declare plans in Inspire
- point the schema at a Stripe secret
- decide whether any plan is per-seat
- define a grace policy
- gate product access with
principal.plan - use the generated payment API for checkout, portal access, and subscription reads
That gives you one shared payment vocabulary across the schema, runtime, and app code.
Start With A Small Plan Model
payments {
@secret stripeKey
plan Free {}
plan Pro {
stripe: {
monthly: "price_pro_monthly",
annual: "price_pro_annual",
},
}
gracePeriod: 7d,
graceAction: downgrade,
}Start with the product plans your app actually needs. You can always grow the payment model later, but vague plans and Stripe IDs scattered through the codebase get hard to reason about quickly.
Keep Feature Gating In Product Terms
The point of the payments model is that the rest of your app should not care about Stripe Price IDs.
Use plan-aware access in product language:
entity AdvancedReport {
title: string,
@grant read where principal.plan in [Pro, Team]
}And use runtime checks in server code when the rule belongs to a workflow rather than raw data access:
const sub = await inspire.payments.getSubscription();
if (sub.plan === "Free") {
throw new ClientError("Upgrade required to export reports.");
}That split stays clean:
- policies protect data
- application code decides feature behavior
- the payment system supplies the shared plan state
Use Per-Seat Billing Only When Seats Are Real Rows
Per-seat billing works best when a seat is already a first-class entity in your schema.
payments {
@secret stripeKey
plan Team {
stripe: "price_team_monthly",
perSeat: true,
}
seatEntity: OrgMembership,
gracePeriod: 7d,
graceAction: downgrade,
}That way the billing quantity follows your actual membership model instead of an application-maintained counter that can drift.
If your app does not already have a real entity that represents seats, define that model first. Seat billing should follow the product shape, not invent it.
Model Failure Behavior Up Front
Grace handling is not an edge case. It is part of the product.
Decide early whether failed payments should:
- downgrade access after the grace window, or
- move the account into a suspended state that your app treats differently
Being explicit here is important because a missing payment-failure policy creates confusion everywhere else: support behavior, UX messaging, policy gating, and internal reporting.
Use Payment Effects For Reactions, Not State
Payment effects are the right place for downstream reactions:
- send a receipt
- notify a team channel
- start a churn workflow
- remind a trial user before expiry
They are not the right place to define subscription truth. Subscription truth should remain in the platform-managed payment layer.
payments {
@secret stripeKey
plan Free {}
plan Pro { stripe: "price_pro_monthly" }
gracePeriod: 7d,
graceAction: downgrade,
@on(payment.succeeded) effect sendReceipt {
retries: 5,
backoff: exponential(1s),
}
}That division of responsibility keeps the system stable:
- the platform owns the billing state
- effects own your reaction to that state
The Runtime Workflow
Once payments are configured, the normal runtime path is:
- your app asks Kizaki to create checkout
- the platform resolves the plan to a Stripe Price
- the platform finds or creates the Stripe customer mapping
- Stripe completes checkout
- the platform handles webhook verification and subscription updates
- your app reads the result through
principal.plan,principal.planStatus, orgetSubscription()
That is what makes managed payments useful. You do not build and re-build the same Stripe glue in every app.
Development Workflow
Most payment work should not require a live Stripe account during normal iteration.
Use the local mock first:
kizaki dev payments set-plan Pro
kizaki dev payments set-status past_due
kizaki dev payments simulate payment.failedThat is enough to validate:
- plan-aware policies
- upgrade prompts
- grace-period UI
- payment lifecycle effects
Use kizaki dev --stripe-live only when you need to test Stripe’s actual behavior.
When To Reach For Raw Stripe
Use raw Stripe directly when the feature is truly provider-specific or outside the managed surface, for example:
- refunds
- promotion-specific workflows
- custom invoicing
- advanced portal configuration
- unusual subscription changes
When you do that, keep one rule in mind: raw Stripe should extend the payment system, not fork it. Use the managed payment state as the app’s source of truth whenever possible.
Recommended Usage
- keep plans simple and product-facing
- do not spread Stripe Price IDs across your codebase
- gate product access through
principal.planandprincipal.planStatus - use managed checkout and subscription state for the default path
- use payment effects for notifications and side effects
- test most payment flows through
kizaki dev paymentsbefore touching live Stripe