# How to use Stripe with Vue.js

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 25 minutes
- Compatibility: Vue 3+, Stripe.js v3, Node.js 18+ backend
- Last updated: March 2026

## TL;DR

Integrate Stripe.js into a Vue.js application by loading the Stripe script, mounting a Payment Element inside a Vue component, and confirming payments with a PaymentIntent client secret from your backend. This guide covers the full flow from form rendering to successful payment confirmation.

## Building a Stripe Payment Form in Vue.js

Stripe.js and Elements let you collect card details securely without sensitive data touching your server. In a Vue.js app, you load Stripe.js, create an Elements instance with a client secret from your backend, and mount the Payment Element inside a Vue component. When the user submits the form, you call stripe.confirmPayment to finalize the charge. This tutorial shows you every step.

## Before you start

- A Vue 3 project created with Vite or Vue CLI
- A backend endpoint that creates a PaymentIntent and returns its client_secret
- Your Stripe publishable key (pk_test_...)
- Basic familiarity with Vue 3 Composition API

## Step-by-step guide

### 1. Install the Stripe.js loader

Use the official @stripe/stripe-js package to load Stripe.js asynchronously. This avoids adding a script tag manually and gives you TypeScript types.

```
npm install @stripe/stripe-js
```

**Expected result:** @stripe/stripe-js is added to your package.json dependencies.

### 2. Create a Stripe composable

Create a composable that initializes the Stripe instance once and provides it to your components. Use your publishable key (pk_test_...) — never the secret key.

```
// src/composables/useStripe.ts
import { loadStripe } from '@stripe/stripe-js';
import type { Stripe } from '@stripe/stripe-js';
import { ref } from 'vue';

const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
const stripe = ref<Stripe | null>(null);

export function useStripe() {
  const init = async () => {
    stripe.value = await stripePromise;
  };

  return { stripe, init };
}
```

**Expected result:** A reusable composable that provides the Stripe instance throughout your Vue app.

### 3. Build the PaymentForm component

Create a component that fetches a PaymentIntent client secret from your backend, mounts the Stripe Payment Element, and handles form submission.

```
<!-- src/components/PaymentForm.vue -->
<template>
  <form @submit.prevent="handleSubmit">
    <div id="payment-element" />
    <button type="submit" :disabled="loading">
      {{ loading ? 'Processing...' : 'Pay now' }}
    </button>
    <p v-if="errorMessage" class="error">{{ errorMessage }}</p>
  </form>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useStripe } from '../composables/useStripe';

const { stripe, init } = useStripe();
const loading = ref(false);
const errorMessage = ref('');
let elements: any = null;

onMounted(async () => {
  await init();
  const res = await fetch('/api/create-payment-intent', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: 2000 })
  });
  const { client_secret } = await res.json();

  elements = stripe.value!.elements({ clientSecret: client_secret });
  const paymentElement = elements.create('payment');
  paymentElement.mount('#payment-element');
});

async function handleSubmit() {
  if (!stripe.value || !elements) return;
  loading.value = true;
  errorMessage.value = '';

  const { error } = await stripe.value.confirmPayment({
    elements,
    confirmParams: {
      return_url: window.location.origin + '/payment-success'
    }
  });

  if (error) {
    errorMessage.value = error.message || 'Payment failed.';
  }
  loading.value = false;
}
</script>
```

**Expected result:** A payment form renders with card fields. Submitting the form confirms the payment and redirects to /payment-success.

### 4. Create the backend PaymentIntent endpoint

Your backend creates a PaymentIntent and returns the client_secret. This Node.js Express example uses the Stripe secret key (sk_test_...) which must stay server-side only.

```
// server.js (Node.js + Express)
const express = require('express');
const Stripe = require('stripe');
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();

app.use(express.json());

app.post('/api/create-payment-intent', async (req, res) => {
  try {
    const intent = await stripe.paymentIntents.create({
      amount: req.body.amount, // amount in cents
      currency: 'usd',
      automatic_payment_methods: { enabled: true }
    });
    res.json({ client_secret: intent.client_secret });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.listen(3001, () => console.log('Server on port 3001'));
```

**Expected result:** POST /api/create-payment-intent returns { client_secret: 'pi_..._secret_...' }.

### 5. Test the payment flow

Run both your Vue dev server and your backend. Use the test card number 4242424242424242 with any future expiry and any CVC to complete a test payment.

```
// Test card:
// Number: 4242 4242 4242 4242
// Expiry: 12/34
// CVC: 123
```

**Expected result:** The payment succeeds, you are redirected to /payment-success, and the PaymentIntent appears as succeeded in your Stripe test Dashboard.

## Complete code example

File: `src/components/PaymentForm.vue`

```vue
<!-- src/components/PaymentForm.vue -->
<template>
  <div class="payment-container">
    <h2>Complete Your Payment</h2>
    <form @submit.prevent="handleSubmit">
      <div id="payment-element" />
      <button type="submit" :disabled="loading || !ready">
        {{ loading ? 'Processing...' : `Pay $${(amount / 100).toFixed(2)}` }}
      </button>
      <p v-if="errorMessage" class="error">{{ errorMessage }}</p>
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { loadStripe } from '@stripe/stripe-js';
import type { Stripe, StripeElements } from '@stripe/stripe-js';

const props = defineProps<{ amount: number }>();

const loading = ref(false);
const ready = ref(false);
const errorMessage = ref('');
let stripe: Stripe | null = null;
let elements: StripeElements | null = null;

onMounted(async () => {
  stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
  if (!stripe) {
    errorMessage.value = 'Failed to load Stripe.';
    return;
  }

  const res = await fetch('/api/create-payment-intent', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: props.amount })
  });

  if (!res.ok) {
    errorMessage.value = 'Could not initialize payment.';
    return;
  }

  const { client_secret } = await res.json();
  elements = stripe.elements({
    clientSecret: client_secret,
    appearance: { theme: 'stripe' }
  });

  const paymentElement = elements.create('payment');
  paymentElement.mount('#payment-element');
  paymentElement.on('ready', () => { ready.value = true; });
});

async function handleSubmit() {
  if (!stripe || !elements) return;
  loading.value = true;
  errorMessage.value = '';

  const { error } = await stripe.confirmPayment({
    elements,
    confirmParams: {
      return_url: `${window.location.origin}/payment-success`
    }
  });

  if (error) {
    errorMessage.value = error.message || 'An unexpected error occurred.';
  }
  loading.value = false;
}
</script>

<style scoped>
.payment-container {
  max-width: 480px;
  margin: 2rem auto;
  padding: 2rem;
}
#payment-element {
  margin-bottom: 1.5rem;
}
button {
  width: 100%;
  padding: 0.75rem;
  background: #635bff;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  cursor: pointer;
}
button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.error {
  color: #df1b41;
  margin-top: 0.75rem;
}
</style>
```

## Common mistakes

- **Using the secret key (sk_test_) in frontend Vue code** — undefined Fix: Only use the publishable key (pk_test_) on the client. The secret key must remain on your server.
- **Mounting the Payment Element before the client secret is available** — undefined Fix: Always await the API call that returns the client_secret before calling stripe.elements().
- **Not handling the redirect after confirmPayment** — undefined Fix: Stripe redirects the user to return_url after confirmation. Make sure that route exists in your Vue Router config.
- **Forgetting to set VITE_STRIPE_PUBLISHABLE_KEY in environment** — undefined Fix: Create a .env file with VITE_STRIPE_PUBLISHABLE_KEY=pk_test_... and restart the Vite dev server.

## Best practices

- Always use the @stripe/stripe-js loader instead of adding a script tag manually
- Keep the Stripe secret key (sk_test_) on your server only — never in Vue components
- Use the Payment Element instead of the Card Element for automatic support of multiple payment methods
- Show a loading state while the Payment Element mounts and during payment confirmation
- Set appearance options on Elements to match your app's design system
- Validate the amount on the server before creating the PaymentIntent
- Handle both the error return from confirmPayment and network failures with try/catch

## Frequently asked questions

### Do I need a backend to use Stripe with Vue.js?

Yes. You need a server-side endpoint to create PaymentIntents using your Stripe secret key. The frontend only handles the UI and payment confirmation.

### Can I use Stripe Elements with Vue 2?

Yes, but you will need to use the Options API instead of the Composition API. The Stripe.js integration itself is framework-agnostic.

### Why use the Payment Element instead of the Card Element?

The Payment Element automatically supports multiple payment methods like cards, Apple Pay, Google Pay, and bank transfers based on your Stripe Dashboard settings. The Card Element only collects card details.

### How do I style the Stripe Payment Element in Vue?

Pass an appearance object when creating the Elements instance. You can set the theme to 'stripe', 'night', or 'flat', and override individual variables like colorPrimary, borderRadius, and fontFamily.

### What happens after stripe.confirmPayment is called?

Stripe redirects the user to the return_url you specified. On that page, you can retrieve the payment status from the URL query parameters using stripe.retrievePaymentIntent.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-use-stripe-with-vue-js
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-use-stripe-with-vue-js
