Your First Payment
Let's build a complete payment form step by step. By the end, you'll understand how the pieces fit together.
What We're Building
A payment form that:
- Loads Stripe.js securely
- Shows Stripe's Payment Element
- Handles the payment submission
Prerequisites
Before we start, make sure you have:
- Vue Stripe installed (see Installation)
- A backend endpoint that creates PaymentIntents
Don't have a backend yet?
You can use the Stripe Dashboard to manually create a PaymentIntent and copy its client secret for testing.
Step 1: Set Up the Provider
Every payment form starts with VueStripeProvider. This component loads Stripe.js and makes it available to all child components.
<script setup>
import { VueStripeProvider } from '@vue-stripe/vue-stripe'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
</script>
<template>
<VueStripeProvider :publishable-key="publishableKey">
<p>Stripe is loading...</p>
</VueStripeProvider>
</template>What's happening:
VueStripeProviderloads Stripe.js asynchronously- While loading, it shows a loading state
- Once ready, it renders the default slot content
- The Stripe instance is now available to all descendants
Step 2: Add the Elements Container
VueStripeElements creates a Stripe Elements instance. This requires a clientSecret from a PaymentIntent.
<script setup>
import { ref } from 'vue'
import { VueStripeProvider, VueStripeElements } from '@vue-stripe/vue-stripe'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('') // We'll fetch this from our backend
// Fetch client secret when component mounts
onMounted(async () => {
const response = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000 }) // $10.00
})
const data = await response.json()
clientSecret.value = data.clientSecret
})
</script>
<template>
<VueStripeProvider :publishable-key="publishableKey">
<VueStripeElements v-if="clientSecret" :client-secret="clientSecret">
<p>Elements loaded!</p>
</VueStripeElements>
<p v-else>Loading payment form...</p>
</VueStripeProvider>
</template>What's happening:
- We fetch a
clientSecretfrom our backend VueStripeElementsonly renders once we have the secret- It creates an Elements instance configured for this payment
Step 3: Add the Payment Element
VueStripePaymentElement renders Stripe's all-in-one payment UI:
<script setup>
import { ref, onMounted } from 'vue'
import { VueStripeProvider, VueStripeElements, VueStripePaymentElement } from '@vue-stripe/vue-stripe'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('')
onMounted(async () => {
const response = await fetch('/api/create-payment-intent', {
method: 'POST',
body: JSON.stringify({ amount: 1000 })
})
const data = await response.json()
clientSecret.value = data.clientSecret
})
</script>
<template>
<VueStripeProvider :publishable-key="publishableKey">
<VueStripeElements v-if="clientSecret" :client-secret="clientSecret">
<VueStripePaymentElement />
<button>Pay $10.00</button>
</VueStripeElements>
<p v-else>Loading...</p>
</VueStripeProvider>
</template>At this point, you should see a payment form with credit card fields (and other payment methods if enabled in your Stripe dashboard).
Step 4: Handle the Payment
Now let's wire up the submit button. We use useStripe() and useStripeElements() to access the instances:
<script setup>
import { ref, onMounted } from 'vue'
import {
StripeProvider,
StripeElements,
StripePaymentElement,
useStripe,
useStripeElements
} from '@vue-stripe/vue-stripe'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('')
const loading = ref(false)
const errorMessage = ref('')
// ... fetch clientSecret in onMounted ...
const handleSubmit = async () => {
const { stripe } = useStripe()
const { elements } = useStripeElements()
if (!stripe.value || !elements.value) {
return
}
loading.value = true
errorMessage.value = ''
const { error } = await stripe.value.confirmPayment({
elements: elements.value,
confirmParams: {
return_url: `${window.location.origin}/payment-complete`
}
})
if (error) {
// Show error to customer
errorMessage.value = error.message || 'Payment failed'
loading.value = false
}
// If successful, Stripe redirects to return_url
}
</script>
<template>
<VueStripeProvider :publishable-key="publishableKey">
<VueStripeElements v-if="clientSecret" :client-secret="clientSecret">
<form @submit.prevent="handleSubmit">
<VueStripePaymentElement />
<div v-if="errorMessage" class="error">
{{ errorMessage }}
</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Processing...' : 'Pay $10.00' }}
</button>
</form>
</VueStripeElements>
<p v-else>Loading payment form...</p>
</VueStripeProvider>
</template>Complete Example
Here's the full payment form:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import {
StripeProvider,
StripeElements,
StripePaymentElement,
useStripe,
useStripeElements
} from '@vue-stripe/vue-stripe'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const clientSecret = ref('')
const loading = ref(false)
const errorMessage = ref('')
onMounted(async () => {
try {
const response = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000 })
})
const data = await response.json()
clientSecret.value = data.clientSecret
} catch (err) {
errorMessage.value = 'Failed to initialize payment'
}
})
const handleSubmit = async () => {
const { stripe } = useStripe()
const { elements } = useStripeElements()
if (!stripe.value || !elements.value) return
loading.value = true
errorMessage.value = ''
const { error } = await stripe.value.confirmPayment({
elements: elements.value,
confirmParams: {
return_url: `${window.location.origin}/payment-complete`
}
})
if (error) {
errorMessage.value = error.message || 'Payment failed'
loading.value = false
}
}
</script>
<template>
<div class="payment-form">
<h2>Complete Your Payment</h2>
<VueStripeProvider :publishable-key="publishableKey">
<VueStripeElements v-if="clientSecret" :client-secret="clientSecret">
<form @submit.prevent="handleSubmit">
<VueStripePaymentElement />
<div v-if="errorMessage" class="error">
{{ errorMessage }}
</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Processing...' : 'Pay $10.00' }}
</button>
</form>
</VueStripeElements>
<template v-else>
<div v-if="errorMessage" class="error">{{ errorMessage }}</div>
<p v-else>Loading payment form...</p>
</template>
</VueStripeProvider>
</div>
</template>
<style scoped>
.payment-form {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
form {
display: flex;
flex-direction: column;
gap: 16px;
}
button {
background: #635bff;
color: white;
border: none;
padding: 12px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #dc3545;
font-size: 14px;
}
</style>Testing Your Payment
Use Stripe's test card numbers:
- Success:
4242 4242 4242 4242 - Requires authentication:
4000 0025 0000 3155 - Declined:
4000 0000 0000 0002
Use any future expiry date and any 3-digit CVC.
What's Next?
Now that you have a working payment form, let's understand how the pieces work together:
- Understanding the Architecture - Learn about StripeProvider, VueStripeElements, and the data flow
- Choosing Your Approach - Decide between Payment Element, Card Element, or Checkout
