The setup is right, the follow-through is missing. Two details.
When you create the SetupIntent, set usage: 'off_session'. If it was created as on_session, the card is saved but the bank was never told to expect a later charge, and European cards then decline it.
When you charge, you must handle the authentication_required error. That is not a bug, it is the customer's bank asking for 3D Secure, and nobody is there to do it:
try {
await stripe.paymentIntents.create({ amount: 1500, currency: 'eur', customer, payment_method, off_session: true, confirm: true })
} catch (e) {
if (e.code === 'authentication_required') {
// mail the customer a link to e.paymentIntent.client_secret to confirm
}
}Without that branch the charge fails silently, which is what your three no-shows look like.