2025, Oct 16 23:00

Fix Stripe subscription payments: use Invoice confirmation_secret instead of latest_invoice.payment_intent (Basil update)

Creating subscriptions with default_incomplete? Basil removed invoice.payment_intent. Learn to confirm the first payment using Invoice confirmation_secret.

When creating a subscription in Stripe and trying to confirm the initial payment on the frontend, many developers reach for the PaymentIntent’s client_secret through the first invoice. If you’re expanding latest_invoice.payment_intent and still hitting AttributeError on payment_intent, you’ve bumped into a recent breaking change.

Example that triggers the issue

The subscription is created with payment_behavior set to default_incomplete and an expand targeting the PaymentIntent on the latest invoice. Accessing payment_intent.client_secret then fails.

import stripe
stripe.api_key = 'sk_test_...'
unit_price = 'price_...'
plan_sub = stripe.Subscription.create(
    customer=acct_user.id,
    items=[{'price': unit_price}],
    payment_behavior='default_incomplete',
    expand=['latest_invoice.payment_intent'],
)
secret_for_ui = plan_sub.latest_invoice.payment_intent.client_secret

What’s really happening and why

Stripe introduced a major API update (Basil) that changes how invoices relate to payments. The stripe-python SDK tracks the latest API version, so using a current SDK means you are on that newer API surface. In Basil, Stripe removed the direct connection between an Invoice and a single PaymentIntent to enable multiple partial payments for one Invoice. The result is that latest_invoice.payment_intent no longer exists on newly created objects even when expanded, which explains the AttributeError.

Stripe’s changelog describes this shift and the migration path. Instead of pulling client_secret from a PaymentIntent on the invoice, you should read the new confirmation_secret directly from the Invoice. See the general changelog at docs.stripe.com/changelog and the specific entry for this change at the Basil changelog. The new property is documented at Invoice.confirmation_secret.

The fix and how to obtain the secret for the frontend

Request the confirmation_secret from the Invoice instead of the PaymentIntent. Expand latest_invoice.confirmation_secret when creating the subscription, then read it off the returned object to use on the frontend.

plan_sub = stripe.Subscription.create(
    customer=acct_user.id,
    items=[{'price': unit_price}],
    payment_behavior='default_incomplete',
    expand=['latest_invoice.confirmation_secret'],
)
ui_token = plan_sub.latest_invoice.confirmation_secret

This gives you the token you need to complete the payment flow on the client.

Why this matters

Payment flows tied to subscriptions often hinge on the very first charge. If your code assumes a PaymentIntent lives under latest_invoice, it will break under the Basil API. Migrating to confirmation_secret aligns your integration with support for multiple partial payments and avoids runtime errors, while keeping your frontend confirmation logic unblocked.

Conclusion

If you are creating subscriptions with payment_behavior set to default_incomplete and need a secret for client-side confirmation, stop expanding latest_invoice.payment_intent and start expanding latest_invoice.confirmation_secret. Keep an eye on Stripe’s changelog, especially for major versions like Basil, to catch breaking changes early and keep your payment flows stable.

The article is based on a question from StackOverflow by Br0k3nS0u1 and an answer by koopajah.