Skip to content

Embed Stripe Checkout ​

Put Stripe's prebuilt Checkout page inside your own page with VueStripeEmbeddedCheckout. Stripe renders the full form (line items, totals, payment methods, addresses, tax IDs, promotion codes) in an iframe, and the customer never leaves your site. To compare it with hosted Checkout and your own Elements form, see Choosing an integration.

Before you begin ​

  • Vue Stripe is installed and its stylesheet imported (Install and set up).
  • A Stripe secret key on your server and a Price (price_...) in your Stripe account.
  • Your app can reach your server's /api routes during development (Send API requests to your server).

How the steps fit together ​

mermaid
sequenceDiagram
    accTitle: Embedded Checkout payment sequence
    accDescr: The browser asks your server for a session, the server creates it with ui_mode embedded_page and returns the client secret, the browser mounts createEmbeddedCheckoutPage, the customer pays inside the iframe, and Stripe redirects to return_url or calls onComplete.
    participant Browser
    participant Server
    participant Stripe
    Browser->>Server: POST /api/embedded-checkout-session
    Server->>Stripe: checkout.sessions.create({ ui_mode: 'embedded_page' })
    Stripe-->>Server: session.client_secret
    Server-->>Browser: clientSecret
    Browser->>Stripe: createEmbeddedCheckoutPage({ clientSecret })
    Note over Browser: Customer pays inside the iframe
    Stripe-->>Browser: redirect to return_url (or onComplete)

1. Create the session on your server ​

Create the session with ui_mode: 'embedded_page' and a return_url, and send client_secret to the browser. The route has its own path because the other Checkout guides create sessions with a different ui_mode, so one app can follow several of them.

js
// Node.js (Express-style handler)
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

app.post('/api/embedded-checkout-session', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    ui_mode: 'embedded_page',
    mode: 'payment',
    line_items: [{ price: 'price_123', quantity: 1 }],
    client_reference_id: 'order_123', // your own order ID, read back by the webhook
    return_url: 'https://example.com/checkout/complete?session_id={CHECKOUT_SESSION_ID}'
  })

  res.json({ clientSecret: session.client_secret })
})

Stripe replaces {CHECKOUT_SESSION_ID} with the session ID when it redirects.

2. Render the component ​

Put VueStripeEmbeddedCheckout inside VueStripeProvider. Pass either a client-secret you already have, or a fetch-client-secret function that Stripe calls when it creates the page:

vue
<!-- CheckoutPage.vue -->
<script setup lang="ts">
import { VueStripeProvider, VueStripeEmbeddedCheckout } from '@vue-stripe/vue-stripe'
import '@vue-stripe/vue-stripe/style.css' // once, e.g. in main.ts

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY

const fetchClientSecret = () =>
  fetch('/api/embedded-checkout-session', {
    method: 'POST',
    signal: AbortSignal.timeout(15000) // give up on a request that never answers
  })
    .then((response) => response.json())
    .then((data) => data.clientSecret)
</script>

<template>
  <VueStripeProvider :publishable-key="publishableKey">
    <VueStripeEmbeddedCheckout :fetch-client-secret="fetchClientSecret">
      <template #loading>Loading checkout...</template>
      <template #error="{ error }">Checkout could not load: {{ error }}</template>
    </VueStripeEmbeddedCheckout>
  </VueStripeProvider>
</template>

When the request fails, or gets no answer within 15 seconds, the component renders its #error slot instead of loading forever (Set a time limit on requests that never answer).

That is the whole integration. Stripe handles the payment form, validation, 3D Secure and wallets.

3. Handle completion ​

After a successful payment Stripe redirects the customer to your return_url. On that page, read session_id from the URL and ask your server for the session's status:

js
// GET /api/checkout-status?session_id=cs_test_...
app.get('/api/checkout-status', async (req, res) => {
  try {
    const session = await stripe.checkout.sessions.retrieve(req.query.session_id)
    res.json({ status: session.status, paymentStatus: session.payment_status })
  } catch {
    res.status(404).json({ error: 'No such Checkout Session' })
  }
})
ts
// GET /api/checkout-status?session_id=cs_test_...
app.get('/api/checkout-status', async (req, res) => {
  try {
    // req.query values can be arrays or objects; String() gives retrieve() a string
    const session = await stripe.checkout.sessions.retrieve(String(req.query.session_id))
    res.json({ status: session.status, paymentStatus: session.payment_status })
  } catch {
    res.status(404).json({ error: 'No such Checkout Session' })
  }
})

status: 'complete' and payment_status: 'paid' mean the payment went through; Redirect to Stripe Checkout has a return page that reads this route. Fulfil orders from the checkout.session.completed webhook, not from the return page, so a closed browser tab does not lose an order.

Stay on the page ​

To show your own success message instead of redirecting, set redirect_on_completion when you create the session in step 1:

  • 'if_required': keep return_url. Cards and wallets complete on the page; payment methods that need a redirect, such as bank authentication, still return to return_url.
  • 'never': remove return_url, because Stripe rejects a session that has both. Stripe then offers no payment methods that need a redirect.
js
const session = await stripe.checkout.sessions.create({
  ui_mode: 'embedded_page',
  mode: 'payment',
  line_items: [{ price: 'price_123', quantity: 1 }],
  redirect_on_completion: 'never' // no return_url
})

Then listen for @complete in CheckoutPage.vue from step 2:

vue
<!-- CheckoutPage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { VueStripeProvider, VueStripeEmbeddedCheckout } from '@vue-stripe/vue-stripe'

const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const done = ref(false)

const fetchClientSecret = () =>
  fetch('/api/embedded-checkout-session', {
    method: 'POST',
    signal: AbortSignal.timeout(15000) // give up on a request that never answers
  })
    .then((response) => response.json())
    .then((data) => data.clientSecret)
</script>

<template>
  <p v-if="done">Thank you for your order.</p>
  <VueStripeProvider v-else :publishable-key="publishableKey">
    <VueStripeEmbeddedCheckout :fetch-client-secret="fetchClientSecret" @complete="done = true" />
  </VueStripeProvider>
</template>

The component destroys the embedded page when v-if removes it.

Switch to another session ​

Stripe allows a single Embedded Checkout on a page at a time. If a second VueStripeEmbeddedCheckout mounts while another one is on the page, it renders its #error slot and emits @error instead of loading. To switch to a different Checkout Session, re-mount the component, for example with :key:

vue
<VueStripeEmbeddedCheckout :key="clientSecret" :client-secret="clientSecret" />

Let customers change quantities ​

Set adjustable_quantity on a line item when you create the session; Stripe shows a quantity picker and updates the total itself:

ts
line_items: [
  { price: 'price_123', quantity: 1, adjustable_quantity: { enabled: true, minimum: 1, maximum: 10 } }
]

The embedded page can't ask your server to approve a line item change: Stripe's API has no session setting that turns on the component's on-line-items-change prop (onLineItemsChange). To change line items from your server during checkout, use the Checkout Form or build your own checkout page, both of which have runServerUpdate().

Track the checkout funnel ​

@analytics forwards Stripe's analytics events (checkoutRendered, deviceData, promotionCodeApplied, lineItemChange, checkoutSubmitted, checkoutSubmitFailed), so you can track the funnel without reaching into the iframe:

vue
<VueStripeEmbeddedCheckout
  :client-secret="clientSecret"
  @analytics="(event) => track(event.eventType, event.details)"
/>

Stripe may send no analytics events at all on a sandbox account: several test setups saw none, through @analytics or a direct stripe.createEmbeddedCheckoutPage({ onAnalyticsEvent }) call, while @complete fired, so you may not be able to test the tracking there.

Test data: see Test your integration.

See also ​