Skip to main content
Stripe’s PaymentElement renders inline inside iframes — including the editor preview — so the whole flow stays in your app: a backend workflow mints a 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. Use payment_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.00 is 2000.
  • Always attach a Customer before paymentIntents.create / subscriptions.create — Stripe rejects Connect-mode payment flows without one. customers.list({ email }) is exact; use customers.search({ query: 'email~"john"' }) for fuzzy lookup.
  • PaymentElement over redirect Checkout — redirect Checkout is blocked by X-Frame-Options in the iframed editor preview. If you must use it, branch on window.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 installed stripe .d.ts when a call fails typecheck.
  • expand maxes at 4 levels, 20 paths per request; list endpoints need the data. 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).