invoice.payment_failed into an email that gets the card updated.
What you will build
Prerequisites
- A Stripe account, and somewhere to host an HTTPS route
- A published for the dunning email
Set it up
1
Install the packages
.env.local
2
Write the route handler
Stripe signs the raw bytes of the request. Read the body as text and hand that exact string to
constructEvent, because parsing it to JSON first changes it and verification fails.app/api/stripe/route.ts
hosted_invoice_url is the field that makes this email work. It is a Stripe-hosted page where the customer pays or updates the card, so your template needs no billing UI of its own.3
Address the right recipient
The snippet above sends to Stamping the id at customer-creation time is the part to get right. Retrofitting the mapping later means reconciling two systems that never agreed on an identifier.
invoice.customer_email, which is the quickest path and needs no profile.It also means Courier has no to apply, so the recipient’s and locale are not considered. For a billing email that is often the point, since dunning is transactional and should reach people who muted marketing.When you do want the profile, store your own user id on the Stripe customer and read it back:app/api/stripe/route.ts
4
Point Stripe at the route
In the Stripe Dashboard, open Workbench → Webhooks, create an event destination for your route’s public URL, and subscribe it to
invoice.payment_failed. Reveal the signing secret and copy it into STRIPE_WEBHOOK_SECRET.Subscribe only to the events you handle. Stripe warns that listening to everything puts avoidable load on your endpoint.Verify
1
Forward events to localhost
2
Trigger a failure
3
Confirm the send
Open . You should see one message with the amount and a
hosted_invoice_url in its data.400, the body was modified before verification. That is the most common cause, and reading it with request.text() rather than request.json() fixes it.
Make it reliable
Stripe retries a failed delivery for up to three days, so a handler that throws will be called again. Three habits keep that from causing damage. Return2xx before slow work. Stripe times out a slow endpoint and counts it as failed. For anything heavier than a single send, acknowledge first and queue the work.
Expect duplicates. The same event can arrive more than once. Stripe’s guidance is to record the event.id values you have processed and skip repeats. Courier’s covers the other half, so pass event.id as the key and a replay cannot send twice.
Do not depend on ordering. Stripe makes no ordering guarantee, so invoice.payment_failed may arrive before the event you expected to precede it.
Adapt it for other events
The trial row pairs well with a . Stripe fires once, three days ahead, and the journey handles the follow-ups from there.