PaymentIntent (or subscription) client_secret, and the frontend confirms it with stripe.confirmPayment(). Reach for redirect Checkout only when you need a hosted page.
Connect the Stripe integration first. The token then arrives as
process.env.ZITE_STRIPE_ACCESS_TOKEN in workflows, and the publishable key as
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY on the frontend.Backend — create a PaymentIntent
Frontend — confirm the payment
Subscriptions
Frontend is identical; only the backend changes. Usepayment_behavior: 'default_incomplete' so it waits for the frontend to confirm, and expand latest_invoice.payment_intent to pull the client_secret in one call:
Gotchas
- Amounts are in cents.
$20.00is2000. - Always attach a Customer before
paymentIntents.create/subscriptions.create— Stripe rejects Connect-mode payment flows without one.customers.list({ email })is exact; usecustomers.search({ query: 'email~"john"' })for fuzzy lookup. - PaymentElement over redirect Checkout — redirect Checkout is blocked by
X-Frame-Optionsin the iframed editor preview. If you must use it, branch onwindow.top !== window.self(new tab when iframed). - Subscription shape moved in SDK v18: billing period lives on the item (
subscription.items.data[0].current_period_end), discounts are an array (subscription.discounts[0]). Read the installedstripe.d.tswhen a call fails typecheck. expandmaxes at 4 levels, 20 paths per request; list endpoints need thedata.prefix (expand: ['data.customer']).- For push updates (renewal, charge succeeded), add an inbound webhook and point Stripe’s webhook config at its URL. Otherwise read on demand with
stripe.subscriptions.retrieve(id).