I am trying to create a Stripe Subscription using the ExpressCheckoutElement for Google Pay and Apple Pay. My backend successfully creates the subscription and returns a clientSecret, but the frontend fails when calling stripe.confirmPayment with a specific error about setup_future_usage.
The same backend logic works perfectly for standard credit card payments using the CardElement.
My backend has an endpoint that creates a Stripe Subscription. To handle potential SCA/3D Secure requirements for card payments, I'm using payment_behavior: 'default_incomplete'.
app.post('/create-subscription-intent', async (req, res) => {
const { customerId, priceId } = req.body;
try {
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
// This parameter is crucial. It's used to handle SCA for card payments
// by leaving the subscription incomplete until confirmed on the client.
payment_behavior: 'default_incomplete',
// We need to expand this to get the client_secret
expand: ['latest_invoice.payment_intent'],
});
// Safely access the client_secret
const clientSecret =
subscription.latest_invoice.payment_intent.client_secret;
res.send({ clientSecret });
} catch (error) {
res.status(400).send({ error: { message: error.message } });
}
});
The Frontend Flow (React / @stripe/react-stripe-js)
On the frontend, I use the ExpressCheckoutElement. When the user confirms their payment in the Google Pay/Apple Pay dialog, the onConfirm event fires. I then take the clientSecret from my backend and use it to confirm the payment.
import {
Elements,
ExpressCheckoutElement,
useStripe,
useElements,
} from '@stripe/react-stripe-js';
function CheckoutForm() {
const stripe = useStripe();
const handleExpressCheckoutConfirm = async () => {
if (!stripe) {
return;
}
// 1. Call backend to create the subscription and get the clientSecret
const { error: backendError, clientSecret } = await fetch(
'/create-subscription-intent',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
/* customerId, priceId */
}),
}
).then((res) => res.json());
if (backendError) {
console.error(backendError.message);
return;
}
// 2. Use the clientSecret to confirm the payment on the frontend
const { error: stripeError } = await stripe.confirmPayment({
clientSecret,
confirmParams: {
// The return URL is required for redirect-based payment methods
return_url: `${window.location.origin}/checkout-success`,
},
});
if (stripeError) {
// This is where the error occurs
console.error(stripeError.message);
}
};
return (
<ExpressCheckoutElement onConfirm={handleExpressCheckoutConfirm} />
);
}
The Problem: The API Error
When stripe.confirmPayment is called in the handleExpressCheckoutConfirm function, the Stripe API returns a 400 Bad Request with the following JSON error payload:
{
"error": {
"message": "The provided setup_future_usage (off_session) does not match the expected setup_future_usage (null). Try confirming with a Payment Intent that is configured to use the same parameters as Stripe Elements.",
"param": "setup_future_usage",
"type": "invalid_request_error"
}
}
My investigation suggests the following is happening:
When I create a Stripe.Subscription with payment_behavior: 'default_incomplete', Stripe implicitly creates the underlying PaymentIntent with setup_future_usage: 'off_session'. This makes sense, as the goal of a subscription is to save the payment method for future, off-session charges.
The ExpressCheckoutElement's confirmation flow, however, seems to require that the PaymentIntent it is confirming has setup_future_usage: null, as explicitly stated in the error message.
What is the correct, Stripe-recommended way to create an initial subscription payment with the ExpressCheckoutElement that also saves the payment method for future recurring (off-session) charges?
because this intent is coming from a subscription's payment intent, you will want to set mode: 'subscription' when setting up your elements instance on your page. That should ensure that the ECE uses the proper setup future usage setting when confirming the intent.
const options = {
mode: 'subscription',
amount: 1099,
currency: 'USD'
};
return (
<Elements stripe={stripePromise} options={options}>
<CheckoutForm />
</Elements>
For one-time payments, setup_future_usage can be directly when initiating your Elements instance via the setupFutureUsage property:
const options = {
mode: 'payment',
amount: 1099,
currency: 'USD',
setupFutureUsage: 'off_session'
};
...