Accept SEPA Direct Debit
Collect an IBAN with VueStripeIbanElement and confirm a SEPA Direct Debit payment with useConfirmSepaDebitPayment(). The element formats and validates the IBAN as the customer types and reports the bank and country.
SEPA Direct Debit does not succeed at once: the PaymentIntent goes to processing and settles or fails after a few business days, so fulfil the order from a webhook, not from the confirm result. Payments that settle later explains the timing.
Before you begin
- Vue Stripe is installed and its stylesheet imported (Install and set up).
- SEPA Direct Debit is enabled in the Stripe Dashboard.
- You show the SEPA mandate text next to the form. Stripe gives the required wording in its SEPA Direct Debit guide.
- Your app can reach your server's
/apiroutes during development (Send API requests to your server).
1. Create a SEPA PaymentIntent on your server
// POST /api/sepa-debit-intent (Node, stripe-node)
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const { amount } = await request.json()
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'eur',
allowed_payment_method_types: ['sepa_debit'],
metadata: { orderId: 'order_123' } // your own order ID, read back by the webhook
})
return Response.json({ clientSecret: paymentIntent.client_secret })
}2. Render the providers
VueStripeElements needs no client secret for the IBAN Element.
<!-- SepaPage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { VueStripeProvider, VueStripeElements } from '@vue-stripe/vue-stripe'
import '@vue-stripe/vue-stripe/style.css' // once, e.g. in main.ts
import SepaForm from './SepaForm.vue'
const publishableKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
const submitted = ref(false)
</script>
<template>
<p v-if="submitted">Payment submitted. It is processed within a few business days.</p>
<VueStripeProvider v-else :publishable-key="publishableKey">
<VueStripeElements>
<SepaForm @submitted="submitted = true" />
</VueStripeElements>
</VueStripeProvider>
</template>3. Render the IBAN Element and confirm
Take the Stripe element from the component's exposed element through a template ref and pass it as sepa_debit. SEPA needs the account holder's name and email in billing_details. The IBAN Element shows its own validation message ("The IBAN you entered is incomplete.") above the field, in its #error slot, so the form only shows the errors from your server and from confirming.
<!-- SepaForm.vue: rendered inside VueStripeElements -->
<script setup lang="ts">
import { ref } from 'vue'
import { VueStripeIbanElement, useConfirmSepaDebitPayment } from '@vue-stripe/vue-stripe'
import type { StripeIbanElementChangeEvent } from '@stripe/stripe-js'
const emit = defineEmits<{ (e: 'submitted'): void }>()
const ibanRef = ref<InstanceType<typeof VueStripeIbanElement> | null>(null)
const { confirmSepaDebitPayment, loading: processing } = useConfirmSepaDebitPayment()
const name = ref('')
const email = ref('')
const ibanComplete = ref(false)
const bankName = ref('')
const errorMessage = ref('')
const handleChange = (event: StripeIbanElementChangeEvent) => {
ibanComplete.value = event.complete
bankName.value = event.bankName || ''
}
const handleSubmit = async () => {
const iban = ibanRef.value?.element
if (!iban) return
errorMessage.value = ''
const response = await fetch('/api/sepa-debit-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 1000 })
})
if (!response.ok) {
errorMessage.value = 'Could not start the payment. Please try again.'
return
}
const { clientSecret } = await response.json()
const { error, paymentIntent } = await confirmSepaDebitPayment(clientSecret, {
payment_method: {
sepa_debit: iban,
billing_details: { name: name.value, email: email.value }
}
})
if (error) {
errorMessage.value = error.message || 'Payment failed'
} else if (paymentIntent?.status === 'processing') {
emit('submitted')
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="name" placeholder="Account holder name" required />
<input v-model="email" type="email" placeholder="Email" required />
<label>IBAN</label>
<VueStripeIbanElement
ref="ibanRef"
:options="{ supportedCountries: ['SEPA'], placeholderCountry: 'DE' }"
@change="handleChange"
/>
<p v-if="bankName">Bank: {{ bankName }}</p>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<!-- Your SEPA mandate text goes here -->
<button type="submit" :disabled="!ibanComplete || processing">
{{ processing ? 'Processing...' : 'Pay with SEPA Direct Debit' }}
</button>
</form>
</template>placeholderCountry picks the IBAN format of the placeholder, for example DE00 0000 0000 0000 0000 00 for 'DE'. For colors and fonts, pass a Stripe style object in options (Style elements).
4. Fulfil the order when the payment succeeds
Listen for payment_intent.succeeded and payment_intent.payment_failed on your server and fulfil or cancel the order there (Fulfil orders with webhooks).
Test data: see Test your integration.
See also
- IBAN Element API: props, events and options
- Bank debit confirmers:
useConfirmSepaDebitPaymentanduseConfirmSepaDebitSetup - Live demo
- For iDEAL, P24, EPS and other bank payments, use the Payment Element; Stripe removed their standalone elements.
