subscription to parent.subscription_details.subscription. A plugin that reads only the old field accepts each renewal notification, finds no subscription in it, and records nothing. The donors' money is safe in Stripe. Update the plugin, leave the webhook endpoint alone, then add the missing renewals with their real dates.This failure is quieter than the usual broken webhook. Stripe's delivery log is green. Every event got a 200 response. Nothing errors, nothing emails you, and the only symptom is that your monthly giving total in WordPress is lower than the money landing in your bank. If even the first gift is missing, you have a different problem: start with our guide to Stripe donations not showing in WordPress. This guide covers the narrower case where month one works and month two onward does not. It applies whichever plugin you run.
Why does the first monthly gift record but month two does not?
Because the first gift and the renewals reach your site by different routes. The first payment happens while the donor is on your donation form, so most plugins record it as part of the checkout itself. Every payment after that is created by Stripe Billing on its own schedule, with nobody on your website, and the only way your site hears about it is a webhook such as invoice.paid carrying the renewal invoice.
Stripe labels those invoices: the Invoice reference gives billing_reason the value subscription_create when "A new subscription was created" and subscription_cycle when "A subscription advanced into a new period." To record a subscription_cycle invoice, a plugin has to work out which donor and which recurring gift it belongs to, and it does that by reading the subscription ID on the invoice. If it looks in a place that no longer exists, it finds nothing. A well-behaved handler that cannot match an event returns 200 and moves on, which is why the log stays green.
What did Stripe change in API version 2025-03-31.basil?
Two breaking changes to the Invoice object. Stripe's changelog entry "Invoicing resources now specify how they were generated" introduces a new parent field that, in Stripe's words, "replaces top level fields that we previously used to reference these objects." Its instruction to developers is direct: "Use invoice.parent.subscription_details.subscription (verify invoice.parent.type is subscription_details) instead of invoice.subscription."
A second entry, "Adds support for multiple (partial) payments on invoices", "Removed the payment_intent, charge, paid, and paid_out_of_band fields from the Invoice object" and "Removed the invoice field from the PaymentIntent and Charge objects." Payments now link to invoices through a separate Invoice Payment object.
| What a plugin needs | Before basil | 2025-03-31.basil and later |
|---|---|---|
| Which subscription an invoice belongs to | invoice.subscription | invoice.parent.subscription_details.subscription |
| Subscription metadata on the invoice | invoice.subscription_details.metadata | invoice.parent.subscription_details.metadata |
| The payment behind an invoice | invoice.payment_intent, invoice.charge | Removed. Listed under invoice.payments, which is only returned when expanded |
| The invoice behind a payment or refund | charge.invoice, payment_intent.invoice | Removed. Look up with GET /v1/invoice_payments |
Here is the part of a renewal invoice that matters, in each shape:
// Before basil
"billing_reason": "subscription_cycle",
"subscription": "sub_1AbC...",
"payment_intent": "pi_3XyZ...",
// 2025-03-31.basil and later
"billing_reason": "subscription_cycle",
"parent": {
"type": "subscription_details",
"subscription_details": { "subscription": "sub_1AbC..." }
}
Basil was a major release. Stripe's versioning page explains: "Twice a year, we issue a new major release (for example, Basil) that starts with an API version containing breaking changes." The current Invoice reference still has no top-level subscription or payment_intent, so every version since basil uses the new shape.
Real integrations broke on this. Outside WordPress, an issue opened on the Craft Commerce Stripe plugin on April 28, 2025 reported the error "This property cannot be expanded because it doesn't exist: payment_intent." (GitHub issue #343, since closed).
My plugin pins an older API version. Why would it see the new shape?
Because webhook payloads are rendered at the webhook endpoint's API version, not at the version a plugin sends with its own API requests. Stripe's API versioning page says: "Webhook events use the API version that's set during your webhook's endpoint creation. Otherwise, they use your Stripe account's default API version."
The webhooks guide is even more explicit that a per-request version does not carry over: if your account is on an older version "and you change the API version for a specific request with versioning, the Event object generated and sent to your destination is still based on" the account's version. Stripe's upgrade guide puts it in one line: the destination's version "is independent of the API version used by your server-side SDK."
On most WordPress donation sites, a person creates the webhook endpoint by hand in the Stripe Dashboard, and Stripe's Dashboard instructions note that "you can choose between your Account's API version or the latest API version." So the plugin never chose the version. Whoever set up the endpoint did, and anyone who picked "latest" after March 31, 2025 got the basil shape or newer. The account default matters too: Stripe says "Your default API version gets set the first time you make an API request" (source). Two sites running the same plugin version can therefore behave differently, which is also why a plugin author's own test site can miss this.
How do I tell if my site is affected?
Compare Stripe's paid renewal invoices with your WordPress records from month two onward, then confirm the endpoint's API version. Three checks, cheapest first.
1. Count renewals on both sides
In the Stripe Dashboard, open Billing, then Subscriptions, and pick a donor whose subscription is at least two months old. Open it and count the paid invoices. Now find the same donor in your donation plugin and count their gifts. If Stripe shows five paid invoices and WordPress shows one gift, you are affected. Check two or three donors whose gifts started at different times, since the date the gap begins tells you when it started.
2. Check the webhook endpoint's API version
In Stripe, open Workbench (Developers on older dashboards), then Webhooks, and click the endpoint that points at your site. The endpoint's details should list the API version it sends events in. If you cannot find it there, open any recent event sent to that endpoint: every Stripe event's JSON carries an api_version field. A version of 2025-03-31.basil or any later date means your renewal events use the new shape. Your account's default version is shown on Workbench's Overview tab.
3. Read one real renewal event
On the same endpoint, open the Event deliveries tab and click an invoice.paid event whose data shows "billing_reason": "subscription_cycle". If the invoice has no top-level subscription field and the subscription ID sits under parent.subscription_details, that is the basil shape. If the delivery shows 200 and the gift is still missing from WordPress, the problem is inside the plugin, not the connection.
What should I do once I know?
Fix the plugin side first, recover the missing records second, and leave the Stripe endpoint as it is.
- Update the plugin, or ask the vendor a precise question. Check the changelog for a fix that mentions the 2025-03-31 API version or the invoice
parentfield. If there is none, send this: "Does your Stripe webhook handler readinvoice.parent.subscription_details.subscriptionfor API version 2025-03-31.basil and later? My endpoint is on version [version] and renewals since [date] are not being recorded." A specific question gets a specific answer much faster than "recurring is broken." - Do not recreate the endpoint or upgrade the API version as a fix. It cannot repair past events, because Stripe states that "You can't change Event objects after creation." A new endpoint created with "latest" can also move a site that currently works onto the basil shape. Upgrading the account default API version in Workbench is risky for the same reason, since endpoints without their own version follow the account default. Stripe's own endpoint upgrade procedure runs an old and a new endpoint side by side with code changes in between. That is a developer task, not a settings tweak.
- Recover the missed renewals, one route per payment. If the plugin is now fixed and the events are recent, resend them. Per the webhooks guide, the Dashboard's Resend button "works for up to 15 days after the event creation," and the Stripe CLI works "for up to 30 days." For anything older, enter the gift by hand, using the invoice's real payment date and amount, with the Stripe payment ID in the reference field. Never resend an event and also add the same payment by hand, or you will count it twice.
- Check refunds too. Under basil a Charge no longer carries an
invoice, so acharge.refundedevent names the payment intent and nothing else. A plugin that saved a renewal under its invoice ID cannot match a later refund of it, so the refunded gift stays counted as income. Compare Stripe's refunded payments against your WordPress statuses for any recurring donor.
In Donor Merchant, manual entry is Donations, then Add donation. Choose "Other / offline" as the method, set the real date, and paste the renewal's payment ID (it starts with pi_) into Reference. That reference becomes the gift's transaction ID, so a later refund notification for that payment finds it. Leave "Email a receipt" unticked unless you want the donor to get one. Be aware that a hand-entered gift is recorded as a one-time gift for that donor, not linked to the subscription. The same screen handles cash and check gifts.
What should I tell donors?
Tell affected donors the truth, briefly: every monthly gift went through, the money arrived, your website's records missed some of them, and you have corrected it. They were not charged twice and do not need to do anything. Something like:
"Thank you for your monthly support. A technical problem on our website meant some of your recent monthly gifts were not shown in our records, although every one was received. We have fixed it, and your giving history is now complete. You do not need to do anything."
The timing that matters most is the year-end statement. A statement built from incomplete records understates what the donor gave, so finish the cleanup before you send statements. If statements for an affected year have already gone out, send corrected ones. Confirm receipt wording with your accountant rather than relying on any plugin's defaults, and see our guide to annual giving statements for the rest of that process.
Was Donor Merchant affected?
Yes. Before version 2.5.3, Donor Merchant read only the old field and had exactly this bug. Our public changelog for 2.5.3 says so: "Stripe subscription renewals were never recorded on newer Stripe accounts. Stripe moved where an invoice reports its subscription in its 2025-03-31 API version, and the plugin only read the old location, so from the second month onward a recurring gift was charged and no record appeared." Version 2.5.3 reads both invoice shapes and deduplicates renewals by invoice, so a resent event is not recorded twice. Version 2.5.4 also saves basil renewals under their payment intent, so a refund of a renewal finds the right gift.
We are telling you this because the fix is only useful if people know to look. If your plugin's vendor has not said whether it handles the new invoice shape, ask. The plugin is free to download with every feature included. If you would rather have someone walk through your Stripe setup with you, that is what our support plans are for.