Add the Checkout Form (beta)
Put Stripe's complete checkout form on your page with VueStripeCheckoutForm: contact details, addresses, shipping, tax ID, express checkout wallets, the payment method, the order total and the Pay button come as one element. A Checkout Session keeps the order, so Stripe calculates totals, tax, discounts and shipping. Stripe.js exposes it as the Checkout Form SDK (stripe.initCheckoutFormSdk(), ui_mode: 'form'). To compare it with the other Checkout integrations, see Choosing an integration.
Beta: requires Stripe access
The Checkout Form is a Stripe beta. Your Stripe account needs access to ui_mode: 'form'; contact Stripe support. Some options have their own gate: the compact layout is a separate private preview. Without access, Stripe's error appears in the component's #error slot instead of the form.
Before you begin
- Your Stripe account has access to the Checkout Form beta (see above).
- Vue Stripe is installed and its stylesheet imported (Install and set up).
- A Stripe secret key on your server and a Price (
price_...) in your Stripe account. - Your app can reach your server's
/apiroutes during development (Send API requests to your server).
How the steps fit together
sequenceDiagram
accTitle: Checkout Form payment sequence
accDescr: The browser asks your server for a session, the server creates it with ui_mode form and returns the client secret, the browser creates the form with initCheckoutFormSdk, and on the confirm event confirms with Stripe, which reports success or redirects to return_url.
participant Browser
participant Server
participant Stripe
Browser->>Server: POST /api/checkout-form-session
Server->>Stripe: checkout.sessions.create({ ui_mode: 'form' })
Stripe-->>Server: session.client_secret
Server-->>Browser: clientSecret
Browser->>Stripe: initCheckoutFormSdk({ clientSecret }), createForm()
Note over Browser: Customer fills the form and clicks Pay
Browser->>Browser: VueStripeCheckoutForm @confirm(event)
Browser->>Stripe: confirm({ formConfirmEvent: event })
Stripe-->>Browser: success, or redirect to return_url1. Create the session on your server
Create the session with ui_mode: 'form' and a return_url. ui_mode: 'form' needs Stripe API version 2026-03-25.dahlia or later; older versions answer "Invalid ui_mode: form". The route has its own path because the other Checkout guides create sessions with a different ui_mode, so one app can follow several of them.
// Node.js (Express-style handler)
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
app.post('/api/checkout-form-session', async (req, res) => {
const session = await stripe.checkout.sessions.create({
ui_mode: 'form',
mode: 'payment',
line_items: [{ price: 'price_123', quantity: 1 }],
client_reference_id: 'order_123', // your own order ID, read back by the webhook
return_url: 'https://example.com/checkout/complete?session_id={CHECKOUT_SESSION_ID}'
})
res.json({ clientSecret: session.client_secret })
})2. Wrap the page in the provider
VueStripeCheckoutFormProvider calls stripe.initCheckoutFormSdk() and makes the session available to the components inside it. Pass the request for the client secret as a function in fetch-client-secret: the provider calls it in the browser only, once Stripe.js has loaded.
<!-- CheckoutPage.vue -->
<template>
<VueStripeProvider :publishable-key="publishableKey">
<VueStripeCheckoutFormProvider
:fetch-client-secret="fetchClientSecret"
:appearance="{ theme: 'stripe' }"
>
<template #error="{ error }">
<p class="error">{{ error }}</p>
</template>
<PayForm />
</VueStripeCheckoutFormProvider>
</VueStripeProvider>
</template>
<script setup lang="ts">
import { VueStripeProvider, VueStripeCheckoutFormProvider } from '@vue-stripe/vue-stripe'
import '@vue-stripe/vue-stripe/style.css' // once, e.g. in main.ts
import PayForm from './PayForm.vue'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const fetchClientSecret = () =>
fetch('/api/checkout-form-session', { method: 'POST' })
.then((res) => res.json())
.then((data: { clientSecret: string }) => data.clientSecret)
</script>client-secret also takes the client secret as a string, or as a Promise that you start at the top of <script setup>. The function is safer: a Promise that rejects before the provider mounts is reported as an uncaught rejection (Errors), and on a server-rendered page (Nuxt, Vite SSR, Quasar SSR) the request starts during server rendering, which logs an error on every render or, inside VueStripeProvider, stops a plain Node SSR server. See Fetch client secrets in the browser.
3. Render the form and confirm
VueStripeCheckoutForm renders the whole form, including the Pay button. When the customer clicks Pay (or authorizes a wallet), the form emits @confirm. Pass that event to confirm() from useCheckoutForm():
<!-- PayForm.vue -->
<template>
<VueStripeCheckoutForm :options="{ layout: 'expanded' }" @confirm="onConfirm" />
<p v-if="message">{{ message }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { VueStripeCheckoutForm, useCheckoutForm } from '@vue-stripe/vue-stripe'
import type { StripeCheckoutFormConfirmEvent } from '@vue-stripe/vue-stripe'
const { confirm } = useCheckoutForm()
const message = ref('')
const onConfirm = async (event: StripeCheckoutFormConfirmEvent) => {
// The session has return_url, so no returnUrl here.
// redirect: 'if_required' keeps card payments on this page.
const result = await confirm({ formConfirmEvent: event, redirect: 'if_required' })
message.value =
result.type === 'error' ? result.error.message : `Payment ${result.session.status.type}`
}
</script>Nothing is charged until you call confirm. Payment methods that need a redirect (bank authentication, some wallets) leave the page and come back to return_url.
Read the form values
@change reports whether the form is complete and its current values; getValue() on a template ref reads them on demand. This version of PayForm.vue shows a hint until the form is complete and names the receipt email after the payment:
<!-- PayForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { VueStripeCheckoutForm, useCheckoutForm } from '@vue-stripe/vue-stripe'
import type {
StripeCheckoutFormChangeEvent,
StripeCheckoutFormConfirmEvent
} from '@vue-stripe/vue-stripe'
const { confirm } = useCheckoutForm()
const formRef = ref<InstanceType<typeof VueStripeCheckoutForm> | null>(null)
const complete = ref(false)
const message = ref('')
const onChange = (event: StripeCheckoutFormChangeEvent) => {
complete.value = event.complete
}
const onConfirm = async (event: StripeCheckoutFormConfirmEvent) => {
const values = await formRef.value?.getValue()
const result = await confirm({ formConfirmEvent: event, redirect: 'if_required' })
message.value =
result.type === 'error'
? result.error.message
: `Payment ${result.session.status.type}. Receipt sent to ${values?.value.customerDetails.email}.`
}
</script>
<template>
<VueStripeCheckoutForm ref="formRef" @change="onChange" @confirm="onConfirm" />
<p v-if="!complete">Fill in every field to pay.</p>
<p v-if="message">{{ message }}</p>
</template>In the compact layout (private preview), the change event also lists the form's views; call formRef.value.setView(index) to move between them.
Place the currency selector yourself
With Adaptive Pricing, the form shows its own currency picker. To place it elsewhere on the page, render a VueStripeCurrencySelectorElement inside the provider; the form then leaves its built-in picker out:
<VueStripeCheckoutFormProvider :fetch-client-secret="fetchClientSecret">
<VueStripeCurrencySelectorElement />
<VueStripeCheckoutForm @confirm="onConfirm" />
</VueStripeCheckoutFormProvider>The selector can go above or below the form. Stripe.js expects the form to exist first, so the selector waits a tick after it mounts and is created after a VueStripeCheckoutForm rendered in the same update. If you show the form later (for example behind a v-if), render the selector with it or after it.
No other element component works inside VueStripeCheckoutFormProvider: the form already contains them.
Change the order
The form collects the email, phone number, addresses and tax ID itself, so useCheckoutForm() has no updateEmail or address updates. It keeps the actions that change the order: applyPromotionCode, updateLineItemQuantity, updateShippingOption and runServerUpdate. (addOptionalLineItem / removeOptionalLineItem exist on the composable but resolve an error: Stripe does not accept optional items on form sessions.) The full list is in the useCheckoutForm API.
Test data: see Test your integration.
See also
- VueStripeCheckoutFormProvider API
- VueStripeCheckoutForm API
- useCheckoutForm API
- Build your own checkout page on a Checkout Session: arrange individual elements instead
- Live demo: pay with the Checkout Form in this site's sandbox
