Monetization
Expiring card reminder email: template and code
Get cards updated before they expire, so a payment never fails over a date.
Passing, tested 2026-10-19 · Email, Inbox
How it works
When to send expiring card reminders
An expiring card reminder gets customers to update a credit card before it expires, so a renewal payment doesn't fail. It emails them 30 days before the expiry date, then sends a second email and an Inbox note a week before. Updating the card stops any reminder that hasn't gone out.
- 30 days before expiryYour billing job starts the journey
- 30 days beforeEmail
- 7 days beforeEmail and Inbox
Stops early: Updating the card cancels every reminder still to come.
The waits are Courier delay nodes. We test this exact journey with each wait shortened to seconds.
Messages
What each step sends
Step 1: When the journey starts, 30 days before the card expires
Email.
Hi Dana,
The Visa card ending in 4242 expires on November 30. Update it now so your next Pro payment goes through.
It takes about a minute.
Other subject lines: "Update your card before November 30", "Your Visa card is about to expire"
Step 2: 23 days after the first email, 7 days before the card expires
Email and Inbox.
Hi Dana,
Your Visa card ending in 4242 expires on November 30. Update it before then so your Pro payment doesn't fail.
In-app Inbox
Update the card ending in 4242 so your next payment goes through.
Other subject lines: "Your Visa card expires on November 30"
Build
Add it to your app
1. Create the journey
Run setup.ts once with your API key. It creates the templates and the journey, publishes it, and prints the journey ID you pass to step 2. Copy setup.ts, journey.json, and the templates folder from the full definition at the end of this section.
Terminal
COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
2. Start it 30 days before a card expires
Node.js
// Start the reminders when a card is 30 days from expiring, with the journey ID// setup.ts printed. Call it from your daily billing job. The first email sends// right away, and the journey's 23-day wait puts the second one a week before// the card expires. The name comes from the user's Courier profile, and the// templates format expiresAt as a date. Runs are tagged with the user's ID, so// stopCardReminders takes the same ID.import Courier from "@trycourier/courier";const client = new Courier();export async function startCardReminders(journeyId: string,userId: string,card: {plan: string;brand: string;last4: string;expiresAt: Date;}) {const { runId } = await client.journeys.invoke(journeyId, {user_id: userId,data: {plan_name: card.plan,card_brand: card.brand,card_last4: card.last4,expires_at: card.expiresAt.toISOString(),},});return runId;}
Python
# Start the reminders when a card is 30 days from expiring, with the journey ID# setup.ts printed. Call it from your daily billing job. The first email sends# right away, and the journey's 23-day wait puts the second one a week before# the card expires. The name comes from the user's Courier profile, and the# templates format expires_at as a date. Runs are tagged with the user's ID, so# stop_card_reminders takes the same ID.from datetime import datetimefrom courier import Courierclient = Courier()def start_card_reminders(journey_id: str, user_id: str, card: dict) -> str:expires_at: datetime = card["expires_at"]response = client.journeys.invoke(template_id=journey_id,user_id=user_id,data={"plan_name": card["plan"],"card_brand": card["brand"],"card_last4": card["last4"],"expires_at": expires_at.isoformat(),},)return response.run_id
3. Stop it when the card is updated
Node.js
// Stop the reminders when the customer updates their card: call this from the// code that saves the new card. It cancels every active run for the user,// and does nothing if the reminders already finished.import Courier from "@trycourier/courier";const client = new Courier();export async function stopCardReminders(userId: string) {await client.journeys.cancel({ cancelation_token: `card-${userId}` });}
Python
# Stop the reminders when the customer updates their card: call this from the# code that saves the new card. It cancels every active run for the user,# and does nothing if the reminders already finished.from courier import Courierclient = Courier()def stop_card_reminders(user_id: str) -> None:client.journeys.cancel(cancelation_token=f"card-{user_id}")
Full definition: journey.json, setup.ts, and the 3 templates
One wait, three sends. The first email goes out when your billing job finds a card 30 days from expiring, then the journey waits 23 days so the second email and Inbox note land a week before.
Every run is tagged with the user's ID, so a card update stops it. Dates show in each recipient's timezone from their profile, and New York when it isn't set.
journey.json
{"name": "Expiring card reminders","cancelation_token": "card-{{recipient}}","nodes": [{"type": "trigger","trigger_type": "api-invoke"},{"type": "send","channel": "email","message": {"template": "@card-expiring-email"}},{"type": "delay","mode": "duration","duration": "P23D"},{"type": "send","channel": "email","message": {"template": "@card-expiring-final-email"}},{"type": "send","channel": "inbox","message": {"template": "@card-expiring-final-inbox"}},{"type": "exit"}]}
setup.ts
// Creates this recipe's journey from journey.json and the templates/ folder,// then publishes it. Run it once per workspace://// COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts//// It prints the journey ID and the template IDs it created.import Courier from "@trycourier/courier";import { readFileSync, readdirSync } from "node:fs";const client = new Courier();const here = new URL(".", import.meta.url);const readJson = (path: string) =>JSON.parse(readFileSync(new URL(path, here), "utf8"));const spec = readJson("journey.json");// 1. Create the journey, then the templates its send nodes use.const journey = await client.journeys.create({name: spec.name,nodes: [spec.nodes[0], spec.nodes.at(-1)],});const templateIds: Record<string, string> = {};for (const file of readdirSync(new URL("templates/", here))) {const key = file.replace(/\.json$/, "");const template = await client.journeys.templates.create(journey.id,readJson(`templates/${file}`));await client.journeys.templates.publish(template.id, {templateId: journey.id,});templateIds[key] = template.id;}// 2. Save the nodes. In journey.json, "@<template>" stands for the ID of the// template with that file name. Node IDs must be unique across a workspace,// so any node ID journey.json names (used by "send_status.<id>" conditions)// gets this journey's ID added.const nodeIds = [...JSON.stringify(spec.nodes).matchAll(/"id":"([\w-]+)"/g),].map((m) => m[1]);let nodes = JSON.stringify(spec.nodes).replace(/"@([\w-]+)"/g, (_, key) =>JSON.stringify(templateIds[key]));for (const id of nodeIds)nodes = nodes.replace(new RegExp(`(?<=[".])${id}(?=")`, "g"),`${id}-${journey.id}`);await client.journeys.replace(journey.id, {name: spec.name,cancelation_token: spec.cancelation_token,nodes: JSON.parse(nodes),});// 3. Publish, so new runs use this version.await client.journeys.publish(journey.id);console.log(JSON.stringify({ journeyId: journey.id, templateIds }));
templates/card-expiring-email.json
{"channel": "email","notification": {"name": "Card expiring soon (email)","tags": ["recipe", "expiring-card"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Your card ending in {{card_last4}} expires soon"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "The {{card_brand}} card ending in {{card_last4}} expires on {{datetime-format expires_at \"MMMM d\" (default profile.timezone \"America/New_York\")}}. Update it now so your next {{plan_name}} payment goes through."},{"type": "action","content": "Update card","href": "https://app.acme.com/billing/card"},{"type": "text","content": "It takes about a minute."}]}]}}}
templates/card-expiring-final-email.json
{"channel": "email","notification": {"name": "Card expires in 7 days (email)","tags": ["recipe", "expiring-card"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Last reminder: your card expires on {{datetime-format expires_at \"MMMM d\" (default profile.timezone \"America/New_York\")}}"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "Your {{card_brand}} card ending in {{card_last4}} expires on {{datetime-format expires_at \"MMMM d\" (default profile.timezone \"America/New_York\")}}. Update it before then so your {{plan_name}} payment doesn't fail."},{"type": "action","content": "Update card","href": "https://app.acme.com/billing/card"}]}]}}}
templates/card-expiring-final-inbox.json
{"channel": "inbox","notification": {"name": "Card expires in 7 days (Inbox)","tags": ["recipe", "expiring-card"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "inbox","elements": [{"type": "meta","title": "Your card expires on {{datetime-format expires_at \"MMMM d\" (default profile.timezone \"America/New_York\")}}"},{"type": "text","content": "Update the card ending in {{card_last4}} so your next payment goes through."},{"type": "action","content": "Update card","href": "https://app.acme.com/billing/card"}]}]}}}
Agents
Hand it to your agent
Give this prompt to Claude Code, Cursor, or Codex with the Courier skill installed. Agents can also read the markdown version and the manifest.
Data
Data fields
| Field | Type | Example | Note |
|---|---|---|---|
| profile.first_name | string | Dana | From the profile. "there" is the fallback when it's missing |
| plan_name | string | Pro | |
| card_brand | string | Visa | |
| card_last4 | string | 4242 | |
| expires_at | string | 2026-11-30T17:00:00Z | An ISO timestamp. The templates format it as a date in the recipient's timezone |
FAQ
Frequently asked questions
How early should I warn customers that their card is expiring?
30 days gives people time to act, and a second reminder a week before the expiry date catches anyone who hasn't. Closer than that leaves little room before a payment fails. To change the gap between the emails, edit the wait in the Journeys editor.
How do I stop the reminders once a customer updates their card?
Each run is tagged with the user's ID, so cancel the token card-<user_id> from the code that saves the new card. Courier stops every reminder still waiting and leaves finished runs alone.
How do I find which cards are about to expire?
Your payment provider returns each card's expiry month and year. Run a daily billing job that looks for cards expiring in 30 days and starts the journey for each customer.
What is involuntary churn, and does this prevent it?
Involuntary churn is a customer lost to a failed payment rather than a decision to cancel. An expired card is a common cause, and an update before the payment date keeps the charge from failing.
Recipes
Related recipes
Recover failed payments with three reminders that stop the moment the card goes through.
Remind annual customers before they renew, so the charge never comes as a surprise.
Send a clean receipt after every payment, by email and in your app's Inbox.
Build this in your workspace
Run setup.ts against a free account and the journey is live in a minute. Prefer no code? Rebuild it in the Journeys editor.