DEV Community

Karan Palukuri
Karan Palukuri

Posted on

Stripe removed invoice.payment_intent in the basil API and it quietly broke my failed-payment workflow

Someone on Reddit tried my free Stripe decline-code workflow on a fresh test account and got nothing back. I figured they'd set it up wrong. They hadn't.

Their account was on API version 2025-03-31.basil. From that version the invoice doesn't have payment_intent, charge or paid anymore. My code read invoice.payment_intent, got undefined, and requested /v1/payment_intents/undefined. Stripe returned a 404 and the workflow just stopped. No retry, no email for that customer, and nothing in the logs that pointed at why.

The fix

Ask for the invoice's payments and expand the payment intent in the same call:

GET /v1/invoice_payments?invoice=in_123&limit=1&expand[]=data.payment.payment_intent
Enter fullscreen mode Exit fullscreen mode

The newest attempt comes first, so data[0].payment.payment_intent is the one you used to read straight off the invoice. From there last_payment_error.decline_code works like before.

Other things that moved

  • invoice.paid (the boolean) is gone. Check status === "paid", or your "did they already pay?" re-check will always say no.
  • Webhook endpoints keep the API version they were created with, so you can get the old and new shapes at once. Keep the old path as a fallback.
  • 3D Secure shows up as requires_action. There's no decline code to classify and retrying does nothing, so send those customers to the hosted invoice page.

I tested all of this against real failing test invoices: a hard decline, a soft decline and a 3DS card. All three route correctly now.

The full write-up is here: https://revguardlabs.org/guides/stripe-invoice-payment-intent-missing-basil

Thanks to akl773 on r/n8n for reporting it.

Top comments (2)

Collapse
 
muhammad_turnergane_7ddc profile image
Muhammad Turner Gane •

The bit about webhook endpoints keeping the API version they were created with is the one that catches people months later. You can upgrade the account and the SDK and still have an older endpoint sending the old shape, so some events have invoice.payment_intent and some don't. Something that has helped me is logging the api_version off every incoming event next to what the handler did with it, so when a field comes back undefined you can see straight away which shape it arrived in.

I'd also make the handler fail loudly when a field it depends on is missing. Calling /v1/payment_intents/undefined and moving on is the worst version of this, because from the outside the workflow looks like it ran.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.