Skip to content

useStripeCheckout

A composable for programmatically redirecting users to Stripe's hosted checkout page.

@stripe/stripe-js v8.x Compatibility

In @stripe/stripe-js v8.x, the redirectToCheckout method was removed. Use redirectToUrl() or redirectToCheckout({ url }) with the Checkout Session URL from your backend instead.

What is useStripeCheckout?

A composable for Stripe Checkout with built-in state management:

CapabilityDescription
URL RedirectRedirect using session URL (v8.x compatible)
Legacy RedirectWraps stripe.redirectToCheckout() (v7.x only)
Loading StateTracks whether redirect is in progress
Error StateCaptures and exposes checkout errors

How It Works

mermaid
flowchart TD
    A["Component calls useStripeCheckout()<br/>Returns { redirectToCheckout, redirectToUrl, loading, error }"] --> B["User triggers checkout<br/><br/>// v8.x: URL-based (recommended)<br/>redirectToUrl('https://checkout.stripe.com/...')<br/>// OR<br/>await redirectToCheckout({ url: 'https://checkout...' })<br/><br/>// v7.x: Session ID-based (legacy)<br/>await redirectToCheckout({ sessionId: 'cs_test_xxx' })"]
    B --> C["Composable sets loading.value = true<br/>Redirects via window.location.replace() or redirectToCheckout"]
    C --> D{Result?}
    D -->|Success| E["<b>SUCCESS</b><br/>User redirected to<br/>Stripe Checkout<br/>(page navigates away)"]
    D -->|Error| F["<b>ERROR</b><br/>loading = false<br/>error.value = message<br/>Throws StripeProviderError"]

Usage Context

This composable must be called within a component that is a descendant of VueStripeProvider. It does not require VueStripeElements.

Usage

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

const { redirectToUrl, loading, error } = useStripeCheckout()
const sessionUrl = ref('')

onMounted(async () => {
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    body: JSON.stringify({ priceId: 'price_xxx' })
  })
  const data = await response.json()
  sessionUrl.value = data.url // Backend returns session URL
})

const handleCheckout = () => {
  redirectToUrl(sessionUrl.value)
}
</script>

<template>
  <button @click="handleCheckout" :disabled="loading || !sessionUrl">
    {{ loading ? 'Redirecting...' : 'Checkout' }}
  </button>
</template>

v7.x (Legacy)

vue
<script setup>
import { useStripeCheckout } from '@vue-stripe/vue-stripe'

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

const handleCheckout = async () => {
  try {
    await redirectToCheckout({
      sessionId: 'cs_test_xxx'
    })
  } catch (err) {
    console.error('Checkout failed:', err.message)
  }
}
</script>

Return Value

ts
interface UseStripeCheckoutReturn {
  redirectToCheckout: (options: CheckoutRedirectOptions | LegacyCheckoutOptions) => Promise<void>
  redirectToUrl: (url: string) => void
  loading: Readonly<Ref<boolean>>
  error: Readonly<Ref<string | null>>
}
PropertyTypeDescription
redirectToUrl(url: string) => voidRedirect using session URL (v8.x, recommended)
redirectToCheckoutFunctionRedirect with options (supports both v7.x and v8.x)
loadingReadonly<Ref<boolean>>Whether redirect is in progress
errorReadonly<Ref<string | null>>Error message from the last redirect attempt

redirectToCheckout Options

URL-Based Options (v8.x Compatible)

ts
interface CheckoutRedirectOptions {
  /** Checkout Session URL from your backend */
  url: string
}

Session-Based Options (v7.x Only)

ts
interface LegacyCheckoutOptions {
  /** Checkout Session ID from your backend */
  sessionId?: string

  /** Line items to purchase */
  lineItems?: Array<{
    price: string
    quantity: number
  }>

  /** Checkout mode */
  mode?: 'payment' | 'subscription' | 'setup'

  /** Redirect URL after successful payment */
  successUrl?: string

  /** Redirect URL when user cancels */
  cancelUrl?: string
}

v7.x Only

sessionId, lineItems, and other legacy options require stripe.redirectToCheckout() which was removed in @stripe/stripe-js v8.x. Use url or redirectToUrl() for v8.x compatibility.

Examples

URL-Based Checkout (v8.x)

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

const { redirectToUrl, loading, error } = useStripeCheckout()
const sessionUrl = ref('')

onMounted(async () => {
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    body: JSON.stringify({ priceId: 'price_xxx' })
  })
  const data = await response.json()
  sessionUrl.value = data.url // Backend returns session URL
})

const handleCheckout = () => {
  redirectToUrl(sessionUrl.value)
}
</script>

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

Session-Based Checkout (v7.x)

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

const { redirectToCheckout, loading, error } = useStripeCheckout()
const sessionId = ref('')

// Create session on mount
onMounted(async () => {
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    body: JSON.stringify({ priceId: 'price_xxx' })
  })
  const data = await response.json()
  sessionId.value = data.sessionId
})

const handleCheckout = async () => {
  try {
    await redirectToCheckout({ sessionId: sessionId.value })
  } catch (err) {
    // User stays on page if redirect fails
    console.error('Checkout failed:', err.message)
  }
}
</script>

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

Multiple Products

v7.x Only

The lineItems/mode/successUrl/cancelUrl options below rely on stripe.redirectToCheckout(), which was removed in @stripe/stripe-js v8.x. On v8.x this call throws. Create a Checkout Session on your server and use redirectToUrl() or redirectToCheckout({ url }) instead.

vue
<script setup>
import { useStripeCheckout } from '@vue-stripe/vue-stripe'

const { redirectToCheckout, loading } = useStripeCheckout()

// Shopping cart items
const cart = ref([
  { priceId: 'price_shirt', quantity: 2 },
  { priceId: 'price_hat', quantity: 1 }
])

const handleCheckout = async () => {
  await redirectToCheckout({
    lineItems: cart.value.map(item => ({
      price: item.priceId,
      quantity: item.quantity
    })),
    mode: 'payment',
    successUrl: `${window.location.origin}/order-complete`,
    cancelUrl: `${window.location.origin}/cart`
  })
}
</script>

Complete Integration Example

v7.x Only

The commented-out price-based (lineItems) option below relies on stripe.redirectToCheckout(), which was removed in @stripe/stripe-js v8.x and throws there. Prefer the backend session flow with redirectToUrl() or redirectToCheckout({ url }).

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
const selectedPlan = ref('')
const isCreatingSession = ref(false)

const plans = [
  { id: 'basic', name: 'Basic', priceId: 'price_basic', price: '$9/mo' },
  { id: 'pro', name: 'Pro', priceId: 'price_pro', price: '$29/mo' },
  { id: 'enterprise', name: 'Enterprise', priceId: 'price_enterprise', price: '$99/mo' }
]
</script>

<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <CheckoutHandler :plans="plans" />
  </VueStripeProvider>
</template>
vue
<!-- CheckoutHandler.vue -->
<script setup>
import { useStripeCheckout } from '@vue-stripe/vue-stripe'

const props = defineProps<{ plans: Array<{ id: string; name: string; priceId: string; price: string }> }>()

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

const handleSelectPlan = async (priceId: string) => {
  try {
    // Option 1: Create session on backend (recommended)
    const response = await fetch('/api/create-checkout-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ priceId })
    })
    const { sessionId } = await response.json()

    await redirectToCheckout({ sessionId })

    // Option 2: Price-based (simpler but less secure)
    // await redirectToCheckout({
    //   lineItems: [{ price: priceId, quantity: 1 }],
    //   mode: 'subscription',
    //   successUrl: `${window.location.origin}/welcome`,
    //   cancelUrl: window.location.href
    // })
  } catch (err) {
    console.error('Failed to start checkout:', err.message)
  }
}
</script>

<template>
  <div class="pricing-grid">
    <div v-for="plan in plans" :key="plan.id" class="plan-card">
      <h3>{{ plan.name }}</h3>
      <p class="price">{{ plan.price }}</p>
      <button @click="handleSelectPlan(plan.priceId)" :disabled="loading">
        {{ loading ? 'Processing...' : 'Select Plan' }}
      </button>
    </div>
  </div>
  <p v-if="error" class="error">{{ error }}</p>
</template>

TypeScript

ts
import { useStripeCheckout } from '@vue-stripe/vue-stripe'
import type { CheckoutRedirectOptions, LegacyCheckoutOptions } from '@vue-stripe/vue-stripe'

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

// URL-based (v8.x compatible, recommended)
const urlCheckout = async (url: string) => {
  const options: CheckoutRedirectOptions = { url }
  await redirectToCheckout(options)
}

// Session-based (v7.x only)
const sessionCheckout = async (sessionId: string) => {
  const options: LegacyCheckoutOptions = { sessionId }
  await redirectToCheckout(options)
}

// Price-based (v7.x only)
const priceCheckout = async (priceId: string) => {
  const options: LegacyCheckoutOptions = {
    lineItems: [{ price: priceId, quantity: 1 }],
    mode: 'payment',
    successUrl: 'https://example.com/success',
    cancelUrl: 'https://example.com/cancel'
  }
  await redirectToCheckout(options)
}

Error Handling

The composable handles errors in two ways:

  1. Stripe Redirect Errors - Stored in error.value and thrown
  2. Initialization Errors - Thrown immediately
vue
<script setup>
import { useStripeCheckout } from '@vue-stripe/vue-stripe'

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

const handleCheckout = async () => {
  try {
    await redirectToCheckout({ sessionId: 'cs_test_xxx' })
    // If successful, user is redirected (this line won't execute)
  } catch (err) {
    // User stays on page
    if (err.message.includes('Stripe not initialized')) {
      // Fixed string thrown by the composable when Stripe hasn't loaded
      console.error('Please wait for Stripe to load')
    } else if (err.message.includes('expired')) {
      // Wording comes from Stripe's result.error.message — match loosely
      await refreshCheckoutSession()
    } else {
      console.error('Checkout error:', err.message)
    }
  }
}
</script>

Common Errors

Stripe not initialized is a fixed string thrown by the composable. The remaining messages come from Stripe's result.error.message (forwarded verbatim into error.value), so the exact wording is determined by Stripe, not Vue Stripe. The phrases below are illustrative examples of Stripe-returned errors.

Error (source)CauseSolution
Stripe not initialized (Vue Stripe)Stripe hasn't loadedWait for StripeProvider to load
Session expired (Stripe result.error.message)Session too oldCreate a new session
Invalid session (Stripe result.error.message)Wrong session IDCheck session ID format
Redirect failed (Stripe result.error.message)Network or Stripe errorShow retry option

useStripeCheckout vs StripeCheckout Component

AspectuseStripeCheckoutStripeCheckout Component
Use CaseCustom UI, programmatic controlQuick integration
UIYou build itPre-styled button
FlexibilityFull controlLimited customization
CodeMore codeLess code

Use useStripeCheckout when you need custom checkout triggers (e.g., form submission, multi-step flow). Use StripeCheckout component for simple button-click checkout.

Requirements

RequirementDetails
StripeProviderMust be used within StripeProvider context
Checkout SessionNeed a session ID or valid Price ID
No StripeElementsDoes not require StripeElements wrapper

Creating Checkout Sessions (Backend)

js
// Node.js example
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 })

  // v7.x: Return session ID (legacy)
  // res.json({ sessionId: session.id })
})

See Also

Released under the MIT License.