Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

Stripe failed payment recovery in Next.js -- handling declined cards, retry logic, and dunning emails with Resend

July 16, 2026
stripenextjsresendsaaspayments

You built the subscription flow. Customers are paying. Then one morning you check your Stripe dashboard and see a cluster of "payment failed" events. A batch of cards got declined -- maybe cards expired, maybe banks rejected the charge, maybe the customer just forgot to update their billing info. Without a recovery plan, those customers quietly disappear from your MRR.

Here is what the recovery window actually looks like and how to wire it up using tools the Next.js SaaS boilerplate already has.

What Stripe does automatically

Stripe's Smart Retry logic retries a failed payment up to 4 times over the next couple of weeks, on a schedule it adjusts based on the failure reason. You do not need to trigger retries yourself -- but you do need to know when they happen so you can update your UI and reach out to the customer.

Every retry attempt fires a webhook event:

  • invoice.payment_failed -- a payment attempt failed
  • invoice.payment_action_required -- the customer needs to authenticate (3D Secure)
  • customer.subscription.deleted -- Stripe finally canceled after all retries failed

These are the three events your webhook handler needs to listen for.

The recovery workflow in three steps

Step 1: Flag the account as at-risk

When invoice.payment_failed arrives, update the user's record in Drizzle ORM with a paymentStatus: 'past_due' field and a paymentFailedAt timestamp. This flag is what drives the in-app banner and email cadence.

await userRepo.update(userId, {
  paymentStatus: 'past_due',
  paymentFailedAt: new Date(),
})

Step 2: Show a payment failure banner in the app

Your authenticated layout already has access to the current user from the JWT. Add a check: if user.paymentStatus === 'past_due', render a banner at the top of every page with a link to your Stripe Customer Portal. The portal lets the customer update their card without you building a payment form.

{user.paymentStatus === 'past_due' && (
  <div className="bg-destructive/10 text-destructive px-4 py-2 text-sm">
    Your last payment failed.{' '}
    <a href="/api/billing/portal" className="underline">
      Update your card
    </a>{' '}
    to keep your account active.
  </div>
)}

Step 3: Send dunning emails with Resend

A banner only helps customers who log in. Most will not check your app daily. Send an email immediately after the first failure, then again on day 3 and day 7 if the subscription is still past due.

The boilerplate's transactional email setup already has Resend wired up. Add a DunningEmail template in emails/ and call it from your webhook handler:

await emailService.sendEmail({
  to: user.email,
  subject: 'Action required -- update your payment method',
  react: React.createElement(DunningEmail, {
    appName: 'Your App',
    updateUrl: portalUrl,
    daysRemaining: 14,
  }),
})

For day 3 and day 7 emails, use a Vercel cron job that queries all users where paymentStatus === 'past_due' and paymentFailedAt is 3 or 7 days ago.

When the subscription cancels

If all retries fail, Stripe fires customer.subscription.deleted. At that point, update the user to paymentStatus: 'canceled', revoke access to paid features, and send a final email -- not a guilt trip, just a clear "your subscription ended, here is how to resubscribe" message.

The usage limits pattern you are probably already using to gate features by plan will automatically block access once the plan field is cleared.

What this recovers

Payment failure recovery is not glamorous infrastructure, but it compounds fast. If 5% of your customers hit a failed payment in a given month and you recover half of them with emails and a portal link, that is 2.5% of MRR you would otherwise lose every month. At $10k MRR, that is $250/month -- and it requires maybe four hours of setup.

The banner, the Resend emails, and the Vercel cron are all things the Next.js SaaS boilerplate already gives you the tools to build. You are not adding new infrastructure -- you are wiring together what is already there.

Get the boilerplate and ship your subscription recovery flow before your next billing cycle.