useCheckout / usePortal and the prebuilt UI are redirect-only - they send the browser to a
provider-hosted page. There is no prop that turns them into an embedded card form. For an embedded
card you build the form with Stripe.js and drive it from the server’s headless
methods (createSetupIntent ->
confirm client-side -> createSubscriptionHeadless); those live on @kerne/server, not here.openCheckout() redirects the browser directly (window.location.href = url). If you want the URL instead - to open in a new tab, or handle the redirect yourself - use createCheckout(), which returns the URL without navigating:
usePortal():
Where the redirect goes
None of these hooks take a URL - checkout success/cancel and the portal return all resolve to the hosted portal, which then hands the session back to your app. See the server SDK’s Checkout docs for the exact resolution and how to configureapp_url/portal_redirect_url.
The caller’s own subscription
The reactive hook covers most cases:Cancelling and switching plans
Both act on the payment provider first, then on Kerne — so a cancellation really stops the next invoice, rather than only closing off access on our side.useSubscription() for you — there is no shared cache to invalidate — so call refetch() once the action resolves if the same screen displays the subscription.
A plan change takes effect immediately, upgrade or downgrade alike, and the provider settles the difference by default. Pass
{ prorationBehavior: 'none' } to skip that and simply charge the new price from the next invoice. Deferring a downgrade to the end of the period is not supported yet.cancelSubscription(id) ends the subscription at the end of the paid period. Pass true as the second argument to end it immediately instead. Either way the row survives with status canceled — cancelling is not deleting, and the billing history stays readable.
Building a plan picker
A page that lets a visitor pick any plan doesn’t know upfront whether they’re a new signup or already subscribed — and that distinction matters more than it looks. Starting a fresh checkout for someone already subscribed doesn’t replace their subscription: Stripe have no notion of “this session replaces the one on file”, so it creates a second, independent subscription while the first keeps billing.usePlanSwitch() makes that decision once, for every screen that needs it — it’s what <PricingTable> and the plan switcher inside <UserProfile> use internally:
subscription is null) sends selectPlan straight to openCheckout() instead — there’s nothing to swap, so pending never gets set for that visitor. switchError, checkoutError and checkoutPriceId (busy state through the checkout redirect) are also on the return value, same idea as isCanceling/isChanging above.
