Stripe invoice.payment_failed webhook in Next.js
A failed renewal should not randomly revoke, resend, or downgrade access. Test the signature, billing reason, dunning state, and duplicate replay before failed-payment traffic reaches production.
Fields to trust
Use the invoice ID, customer ID, subscription ID, attempted amount, currency, attempt count, and hosted invoice URL only after signature verification.
Separate subscription renewal, subscription creation, manual invoice, and update flows so failed first payments do not follow the same path as failed renewals.
Record grace-period, retry, or past-due state explicitly. Avoid deleting access until your policy and Stripe subscription status agree.
Key on the provider event or invoice identity so duplicate replay cannot send repeated dunning email, write duplicate risk rows, or downgrade twice.
Next.js route order
Read await request.text(), verify Stripe-Signature, parse the event, verify the subscription mapping, then hold or mark risk inside an idempotency guard.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("Stripe-Signature");
const event = verifyStripeEvent(rawBody, signature);
if (event.type === "invoice.payment_failed") {
await runOnce(`stripe-payment-failed:${event.data.object.id}`, async () => {
await markSubscriptionPaymentRisk(event.data.object);
});
}
return new Response("ok", { status: 200 });
}
CI gates
- Signed
invoice.payment_failedfixture creates one payment-risk or dunning record. - Mutated raw body returns 400 before any entitlement or email side effect.
- Duplicate replay of the same failed invoice does not send repeated email or repeatedly downgrade access.
- Failed first payment, failed renewal, manual invoice, and unknown
billing_reasonfixtures take separate review paths. customer.subscription.updatedstatus changes do not contradict the failed invoice decision.
FAQ
Should invoice.payment_failed immediately revoke SaaS access?
Usually no. Treat it as a dunning or grace-period signal unless your policy explicitly requires immediate hold. Test the entitlement decision before production.
How do I test duplicate Stripe failed payment webhooks?
Replay the same signed invoice.payment_failed fixture and assert the route writes one risk record, sends at most one dunning action, and does not repeatedly downgrade the account.
Turn failed-payment handling into release evidence
The free fixture and test plan tools cover failed renewals. The CNY 69 Pro Kit is for teams that want fixtures, idempotency tests, entitlement matrices, and review templates already packaged.