You should use the PaymentIntent rather than the Checkout Session alone to determine if an async payment is still in progress. When a Checkout Session expires, it’s possible that a PaymentIntent was already created for an asynchronous payment method. In that case, even though the session “expires,” the PaymentIntent may eventually succeed.
Here’s what to do:
-
Store the PaymentIntent ID: When creating your Checkout Session ensure you keep track of the PaymentIntent ID. If you haven’t already, you can include it in the session’s metadata or look it up later via the session object.
-
Listen for PaymentIntent Events: In addition to handling the checkout.session.expired event, listen for PaymentIntent events (such as payment_intent.succeeded, payment_intent.processing, and payment_intent.payment_failed). The PaymentIntent status reflects the true state of the async payment.
-
Check PaymentIntent Status after Expiration: When you receive checkout.session.expired, check whether the Checkout Session had an associated PaymentIntent. Then retrieve that PaymentIntent and inspect its status:
- If the PaymentIntent is in a “processing” state, it means that Stripe is still working on the async payment and it might succeed.
- If the PaymentIntent is canceled or in error (such as requires_payment_method), then there is no chance for payment completion.
For example:
// Retrieve the expired checkout session
const session = await stripe.checkout.sessions.retrieve(sessionId);
// If a PaymentIntent is present, check its status.
if (session.payment_intent) {
const paymentIntent = await stripe.paymentIntents.retrieve(session.payment_intent);
if (paymentIntent.status === 'processing') {
// The async payment is still in progress. You should wait for its success or failure.
} else if (paymentIntent.status === 'requires_payment_method' ||
paymentIntent.status === 'canceled') {
// No further payment will happen. The customer either did not complete payment or it failed.
} else if (paymentIntent.status === 'succeeded') {
// Payment has been successfully processed.
}
} else {
// No payment was initiated within the session.
}
By relying on the PaymentIntent’s lifecycle states and webhooks, you can distinguish between a Checkout Session that expires while an async payment is processing versus one where no payment method was used at all.