Skip to content

Stripe Checkout

Redirect users to Stripe's hosted checkout page for a quick, secure payment experience without building custom payment UI.

@stripe/stripe-js v8.x Compatibility

In @stripe/stripe-js v8.x, the redirectToCheckout method was removed. Your backend should return the session URL directly, which is then used for window.location.replace(). See the v8.x migration guide for details.

Why Use Checkout?

FeatureBenefit
No custom UIStripe handles the entire payment form
PCI compliantPayment data never touches your servers
Multiple payment methodsCards, wallets, bank transfers automatically
Built-in featuresTaxes, shipping, discounts, receipts included
Mobile optimizedResponsive design works on all devices

When to Use Checkout

ScenarioDescription
Quick integrationGet payments working in minutes
Simple productsOne-time purchases or subscriptions
Global customersStripe auto-shows relevant payment methods
Minimal frontend workDon't want to build payment UI

Checkout vs Payment Element

  • Checkout = Redirect to Stripe-hosted page (simplest)
  • Payment Element = Embed payment UI in your app (customizable)

Choose Checkout when you want the fastest integration with minimal code.

How It Works

mermaid
flowchart TD
    A["<b>1. Backend creates Checkout Session</b><br/>POST /v1/checkout/sessions → { url: 'https://...' }"] --> B["<b>2. Frontend receives session URL</b><br/>Pass to StripeCheckout component or useStripeCheckout"]
    B --> C["<b>3. User clicks checkout button</b><br/>window.location.replace(sessionUrl)"]
    C --> D["<b>4. User redirected to Stripe's checkout page</b><br/>• Enters payment details<br/>• Handles 3D Secure if needed<br/>• Submits payment"]
    D --> E{Result?}
    E -->|Success| F["<b>Payment Successful</b><br/>→ Redirect to success_url"]
    E -->|Cancelled| G["<b>Payment Cancelled</b><br/>→ Redirect to cancel_url"]

Required Components

Checkout only needs VueStripeProvider - no VueStripeElements required:

mermaid
flowchart TD
    A["VueStripeProvider"] --> B["VueStripeCheckout<br/>(button that redirects)"]
ComponentRole
VueStripeProviderLoads Stripe.js
VueStripeCheckoutRenders button, handles redirect

Basic Implementation

Step 1: Create a Checkout Session (Backend)

js
// Node.js / Express
const stripe = require('stripe')('sk_test_...')

app.post('/create-checkout-session', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        price: req.body.priceId,
        quantity: 1
      }
    ],
    mode: 'payment', // or 'subscription'
    success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}',
    cancel_url: 'https://example.com/cancel'
  })

  // v8.x: Return URL (recommended)
  res.json({ url: session.url })
})

Step 2: Add the Checkout Button

vue
<script setup>
import { ref, onMounted } from 'vue'
import {
  VueStripeProvider,
  VueStripeCheckout
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const sessionUrl = ref('')
const loading = ref(true)

// Fetch session on mount
onMounted(async () => {
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ priceId: 'price_xxx' })
  })
  const data = await response.json()
  sessionUrl.value = data.url
  loading.value = false
})
</script>

<template>
  <div v-if="loading">Loading...</div>

  <VueStripeProvider v-else :publishable-key="publishableKey">
    <VueStripeCheckout
      :session-url="sessionUrl"
      button-text="Pay $49.99"
      @error="(err) => console.error(err.message)"
    />
  </VueStripeProvider>
</template>

Using the Composable

For more control, use useStripeCheckout instead of the component:

vue
<script setup>
import { ref, onMounted } from 'vue'
import {
  VueStripeProvider,
  useStripeCheckout
} from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
</script>

<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <CheckoutForm />
  </VueStripeProvider>
</template>
vue
<!-- CheckoutForm.vue (inside StripeProvider) -->
<script setup>
import { ref } from 'vue'
import { useStripeCheckout } from '@vue-stripe/vue-stripe'

const { redirectToUrl, loading, error } = useStripeCheckout()

const handleCheckout = async () => {
  // Create session on backend
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    body: JSON.stringify({ product: 'pro_plan' })
  })
  const { url } = await response.json()

  // Redirect to checkout (v8.x compatible)
  redirectToUrl(url)
}
</script>

<template>
  <button @click="handleCheckout" :disabled="loading">
    {{ loading ? 'Redirecting...' : 'Checkout' }}
  </button>
  <p v-if="error" class="error">{{ error }}</p>
</template>

Subscription Checkout

For recurring payments, set mode: 'subscription':

js
// Backend
const session = await stripe.checkout.sessions.create({
  line_items: [
    {
      price: 'price_monthly_pro', // Recurring price
      quantity: 1
    }
  ],
  mode: 'subscription',
  success_url: 'https://example.com/welcome',
  cancel_url: 'https://example.com/pricing'
})
vue
<!-- Frontend -->
<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <div class="pricing-card">
      <h3>Pro Plan</h3>
      <p>$29/month</p>

      <VueStripeCheckout
        :session-url="proSessionUrl"
        button-text="Subscribe to Pro"
      />
    </div>
  </VueStripeProvider>
</template>

URL-Based vs Session-Based vs Price-Based

Create session on backend, pass session URL to frontend:

vue
<VueStripeCheckout :session-url="sessionUrl" />

Pros:

  • Works with @stripe/stripe-js v8.x
  • Full control over session options
  • Most secure (server-side session creation)
  • Supports all Checkout features

Session-Based (v7.x Legacy)

Create session on backend, pass session ID to frontend:

vue
<VueStripeCheckout :session-id="sessionId" />

Pros:

  • Full control over session options
  • More secure (server-side)

Cons:

  • Requires @stripe/stripe-js v7.x (uses deprecated redirectToCheckout)

Price-Based (v7.x Legacy, Deprecated)

Create session client-side with just a Price ID:

vue
<VueStripeCheckout
  :price-id="priceId"
  mode="payment"
  :success-url="successUrl"
  :cancel-url="cancelUrl"
/>

Pros:

  • No backend needed
  • Quick prototyping

Cons:

  • Requires @stripe/stripe-js v7.x
  • Limited options
  • Less secure
  • Not recommended for production

Handling Success

After payment, Stripe redirects to your success_url:

js
// success_url: 'https://example.com/success?session_id={CHECKOUT_SESSION_ID}'

// Backend: Retrieve the session to confirm payment
app.get('/success', async (req, res) => {
  const session = await stripe.checkout.sessions.retrieve(req.query.session_id)

  if (session.payment_status === 'paid') {
    // Fulfill the order
    await fulfillOrder(session)
    res.render('success')
  }
})

Don't Trust the Redirect

Always verify payment status on your backend. Users could navigate directly to your success URL.

Webhooks

For reliable payment tracking, use webhooks:

js
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature']
  const event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret)

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object
    fulfillOrder(session)
  }

  res.json({ received: true })
})

Customizing Checkout

Session Options

js
const session = await stripe.checkout.sessions.create({
  line_items: [{ price: 'price_xxx', quantity: 1 }],
  mode: 'payment',

  // Customize
  customer_email: 'customer@example.com',
  client_reference_id: 'order_123',

  // Shipping
  shipping_address_collection: {
    allowed_countries: ['US', 'CA', 'GB']
  },

  // Discounts
  allow_promotion_codes: true,

  // Taxes
  automatic_tax: { enabled: true },

  // Branding (Dashboard → Settings → Branding)
  // Custom logo, colors, favicon

  success_url: 'https://example.com/success',
  cancel_url: 'https://example.com/cancel'
})

Button Styling

Use button-class to style the default button:

vue
<VueStripeCheckout
  :session-url="sessionUrl"
  button-class="my-checkout-button"
  button-text="Complete Purchase"
/>

<style>
.my-checkout-button {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  padding: 1rem 2rem;
  border-radius: 8px;
}
</style>

Or replace the button entirely with your own:

vue
<VueStripeCheckout :session-url="sessionUrl">
  <button class="my-custom-button">
    <CreditCardIcon /> Complete Purchase
  </button>
</VueStripeCheckout>

Automatic Click Handling

When you pass custom content, clicking it automatically triggers checkout. No need to wire up click handlers manually.

Error Handling

vue
<script setup>
const handleError = (error) => {
  if (error.message.includes('Session expired')) {
    // Refresh the session
    refreshSession()
  } else if (error.message.includes('Stripe not initialized')) {
    // Wait for Stripe to load
    console.log('Please wait...')
  } else {
    console.error('Checkout error:', error.message)
  }
}
</script>

<template>
  <VueStripeCheckout
    :session-id="sessionId"
    @error="handleError"
  />
</template>

Testing

Test Cards

Card NumberBehavior
4242 4242 4242 4242Succeeds
4000 0000 0000 0002Declined
4000 0025 0000 3155Requires 3D Secure

Use any future expiry date and any 3-digit CVC.

Stripe CLI

Create test sessions without a backend:

bash
# Create a checkout session
stripe checkout sessions create \
  --line-items[0][price]=price_xxx \
  --line-items[0][quantity]=1 \
  --mode=payment \
  --success-url=https://example.com/success \
  --cancel-url=https://example.com/cancel

# Listen for webhooks
stripe listen --forward-to localhost:3000/webhook

Next Steps

Released under the MIT License.