Confirm payments on your server
Collect payment details with the Payment Element, turn them into a ConfirmationToken with useCreateConfirmationToken(), and create and confirm the PaymentIntent on your server. This follows Stripe's finalize payments on the server flow: the Payment Element runs without an Intent, and the Intent only exists once the customer clicks Pay. Payment objects explains how ConfirmationTokens relate to PaymentMethods and Tokens.
Before you begin
- Vue Stripe is installed and its stylesheet imported (Install and set up).
- Your server can create PaymentIntents with your secret key.
- Your app can reach your server's
/apiroutes during development (Send API requests to your server).
ConfirmationToken: confirm on your server
sequenceDiagram
accTitle: ConfirmationToken payment sequence
accDescr: The browser creates a ConfirmationToken from the Payment Element and posts it to your server, the server creates and confirms a PaymentIntent with it and returns the status, and the browser calls handleNextAction when the status is requires_action.
participant Browser
participant Server
participant Stripe
Browser->>Stripe: createConfirmationToken() (submits the Payment Element)
Stripe-->>Browser: ctoken_...
Browser->>Server: POST /api/pay { confirmationToken }
Server->>Stripe: paymentIntents.create({ confirm: true, confirmation_token })
Stripe-->>Server: PaymentIntent (succeeded or requires_action)
Server-->>Browser: { status, clientSecret }, or 402 { error } when declined
Browser->>Stripe: handleNextAction({ clientSecret }) when requires_action1. Render the Payment Element without an Intent
Give VueStripeElements a mode, amount and currency instead of a client secret (Collect payment details before creating an Intent covers this setup):
<!-- CheckoutPage.vue -->
<script setup lang="ts">
import { VueStripeProvider, VueStripeElements, VueStripePaymentElement } from '@vue-stripe/vue-stripe'
import '@vue-stripe/vue-stripe/style.css' // once, e.g. in main.ts
import PayButton from './PayButton.vue'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
</script>
<template>
<VueStripeProvider :publishable-key="publishableKey">
<VueStripeElements mode="payment" :amount="1099" currency="usd">
<VueStripePaymentElement />
<PayButton />
</VueStripeElements>
</VueStripeProvider>
</template>Set amount from the cart before the customer starts typing: changing mode, amount or currency creates a new Elements group, so the Payment Element remounts empty.
2. Create the token and send it to your server
createConfirmationToken() runs elements.submit() first, so a validation error (an incomplete card number, say) resolves as { error } without creating a token. If the server's PaymentIntent needs 3D Secure, useHandleNextAction() finishes it in the browser:
<!-- PayButton.vue: rendered inside <VueStripeElements> -->
<script setup lang="ts">
import { ref } from 'vue'
import { useCreateConfirmationToken, useHandleNextAction } from '@vue-stripe/vue-stripe'
type PayResult = { status?: string; clientSecret?: string; error?: string }
const { createConfirmationToken, loading } = useCreateConfirmationToken()
const { handleNextAction } = useHandleNextAction()
const status = ref('')
const message = ref('')
const pay = async () => {
// Clear the previous attempt's outcome
status.value = ''
message.value = ''
const { confirmationToken, error } = await createConfirmationToken({
params: { return_url: `${window.location.origin}/complete` }
})
if (!confirmationToken) {
message.value = error?.message ?? 'Check your payment details.'
return
}
const response = await fetch('/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ confirmationToken: confirmationToken.id })
})
// A proxy or crash error page is not JSON
const result: PayResult = await response.json().catch(() => ({}))
if (!response.ok || !result.status) {
message.value = result.error ?? 'Payment failed. Please try again.' // declined (402) or server error
return
}
if (result.status === 'requires_action' && result.clientSecret) {
const { paymentIntent, error: actionError } = await handleNextAction({
clientSecret: result.clientSecret
})
if (actionError) {
message.value = actionError.message ?? 'Authentication failed.' // 3D Secure failed or was cancelled
return
}
status.value = paymentIntent?.status ?? 'failed'
} else {
status.value = result.status
}
}
</script>
<template>
<button :disabled="loading" @click="pay">Pay</button>
<p v-if="message">{{ message }}</p>
<p v-if="status">Payment {{ status }}</p>
</template>pay() clears message at the start of each attempt. Don't show the composables' error refs here instead: each one resets only when its own composable runs again, so a failed 3D Secure message would stay on the page after the next payment succeeds.
confirmationToken.payment_method_preview describes what the customer chose (type, card.brand, card.last4, ...) if you want to show it or check it before charging.
3. Create and confirm the PaymentIntent on your server
// Node.js (Express-style handler)
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
app.post('/api/pay', async (req, res) => {
try {
const paymentIntent = await stripe.paymentIntents.create({
amount: 1099, // compute from the cart on the server, never trust the browser
currency: 'usd',
automatic_payment_methods: { enabled: true },
confirm: true,
confirmation_token: req.body.confirmationToken,
return_url: 'https://example.com/complete',
metadata: { orderId: 'order_123' } // your own order ID, read back by the webhook
})
res.json({ status: paymentIntent.status, clientSecret: paymentIntent.client_secret })
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
// A decline: the message is written for the customer
res.status(402).json({ error: err.message })
} else {
console.error(err)
res.status(500).json({ error: 'Payment failed. Please try again.' })
}
}
})Because the server confirms, a decline does not come back as a PaymentIntent status: paymentIntents.create() throws a StripeCardError (HTTP 402, code: 'card_declined', with a decline_code). Its message ("Your card was declined.") is safe to show the customer; other errors are not, so the handler logs them and returns a generic message. Try it with the decline test cards.
The same token confirms a SetupIntent (stripe.setupIntents.create({ confirm: true, confirmation_token })) when you render the Elements group with mode="setup".
Create a PaymentMethod to charge later
useCreatePaymentMethod() creates a reusable PaymentMethod (pm_...) for your server to attach to a customer or charge later. Inside a Payment Element group (with :options="{ paymentMethodCreation: 'manual' }" on VueStripeElements), call it with no arguments; with the Card Element, pass the element. The Card Element shows validation errors (an incomplete card number, say) in its own #error slot, so the sample shows only the other errors:
<script setup lang="ts">
import { ref } from 'vue'
import { VueStripeCardElement, useCreatePaymentMethod } from '@vue-stripe/vue-stripe'
const cardRef = ref<InstanceType<typeof VueStripeCardElement> | null>(null)
const { createPaymentMethod, loading } = useCreatePaymentMethod()
const message = ref('')
const save = async () => {
const card = cardRef.value?.element
if (!card) return
message.value = ''
const { paymentMethod, error } = await createPaymentMethod({ type: 'card', card })
if (paymentMethod) {
// Send paymentMethod.id to your server, e.g. to attach it to a customer
} else if (error && !('type' in error && error.type === 'validation_error')) {
message.value = error.message ?? 'The card could not be saved.'
}
}
</script>
<template>
<VueStripeCardElement ref="cardRef" />
<button :disabled="loading" @click="save">Save card</button>
<p v-if="message">{{ message }}</p>
</template>To save a card with authentication up front, use a SetupIntent instead: see Save a payment method for later.
Tokens and Radar sessions
useCreateToken()wraps everystripe.createToken()overload: card tokens for the legacy Charges API, and account, person and PII tokens for Connect.useRadarSession()gives Radar the browser signals it needs when card details reach Stripe without Stripe.js; pass the session ID to your server asradar_options.session.
See also
- How confirming works: confirming in the browser instead
- Tokens live demo and Create Payment Method demo
- useHandleNextAction API
