Monetization
Trial ending email sequence: template and code
Remind trial users before their trial ends, and stop the moment they upgrade.
Passing, tested 2026-10-19 · Email, Inbox, SMS
How it works
When to send trial ending emails
A trial ending email sequence reminds users that their free trial is about to expire. It sends an email and an Inbox note 7 days before the trial ends, another a day before, with a text if the first email wasn't opened, and a final email when it ends. Upgrading stops the rest.
- Trial startsYour app starts the journey at signup
- 7 days beforeEmail and Inbox
- 1 day beforeEmail and InboxPlus SMS, only if the first email wasn't opened
- Trial endsEmail
Stops early: Upgrade at any point and your app cancels every message 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: 7 days before the trial ends, after the journey's first wait
Email and Inbox.
Hi Dana,
Your Pro trial ends on October 29. Pick a plan to keep everything running with no gap.
Not sure which plan fits? Reply to this email.
In-app Inbox
Pick a plan to keep everything running with no gap.
Other subject lines: "Your Pro trial ends on October 29", "Pick a plan before October 29"
Step 2: 6 days after step 1, 1 day before the trial ends
Email and Inbox. Plus SMS, only if the first email wasn't opened.
Hi Dana,
Your Pro trial ends tomorrow, October 29. Choose a plan today and nothing changes for you or your team.
In-app Inbox
Choose a plan today to keep your account running.
SMS
Acme: your trial ends tomorrow. Keep your account running: https://app.acme.com/billing Reply STOP to opt out.
Other subject lines: "Last day of your Pro trial"
Step 3: 1 day after step 2, when the trial ends
Email.
Hi Dana,
Your Pro trial ended today. Everything you set up is saved, so you can pick up where you left off when you choose a plan.
Other subject lines: "Your trial ended. Your work is still here"
The first wait assumes a 14-day trial (P7D). For another length, set it to the trial length minus 7 days, like P3D for a 10-day trial.
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 when the trial starts
Node.js
// Start the reminders when a trial starts, with the journey ID setup.ts// printed. The journey's first wait (P7D, for a 14-day trial) holds the first// reminder until 7 days before the trial ends, and its later waits time the// rest. For another trial length, change P7D to the trial length minus 7 days.// Names come from the user's Courier profile. Runs are tagged with the user's// ID, so stopTrialReminders takes the same ID.import Courier from "@trycourier/courier";const client = new Courier();export async function startTrialReminders(journeyId: string,userId: string,trial: { plan: string; endsAt: Date }) {const { runId } = await client.journeys.invoke(journeyId, {user_id: userId,data: {plan_name: trial.plan,trial_end_date: trial.endsAt.toISOString(),},});return runId;}
Python
# Start the reminders when a trial starts, with the journey ID setup.ts# printed. The journey's first wait (P7D, for a 14-day trial) holds the first# reminder until 7 days before the trial ends, and its later waits time the# rest. For another trial length, change P7D to the trial length minus 7 days.# Names come from the user's Courier profile. Runs are tagged with the user's# ID, so stop_trial_reminders takes the same ID.from datetime import datetimefrom courier import Courierclient = Courier()def start_trial_reminders(journey_id: str, user_id: str, trial: dict) -> str:ends_at: datetime = trial["ends_at"]response = client.journeys.invoke(template_id=journey_id,user_id=user_id,data={"plan_name": trial["plan"],"trial_end_date": ends_at.isoformat(),},)return response.run_id
3. Stop it when they upgrade
Node.js
// Stop the reminders when the account upgrades: call this from your billing// webhook. It cancels every active run for the user, and does nothing if// the sequence already finished.import Courier from "@trycourier/courier";const client = new Courier();export async function stopTrialReminders(userId: string) {await client.journeys.cancel({ cancelation_token: `trial-${userId}` });}
Python
# Stop the reminders when the account upgrades: call this from your billing# webhook. It cancels every active run for the user, and does nothing if# the sequence already finished.from courier import Courierclient = Courier()def stop_trial_reminders(user_id: str) -> None:client.journeys.cancel(cancelation_token=f"trial-{user_id}")
Full definition: journey.json, setup.ts, and the 6 templates
Three waits and one condition. The journey holds until 7 days before the trial ends, then sends each reminder on schedule. The SMS goes only if the first email wasn't opened and the user has a phone number.
Names come from the user's Courier profile, with "there" as the fallback. Dates show in each recipient's timezone from their profile, and New York when it isn't set. Pass trial_end_date as an ISO timestamp.
journey.json
{"name": "Trial ending reminders","cancelation_token": "trial-{{recipient}}","nodes": [{"type": "trigger","trigger_type": "api-invoke"},{"type": "delay","mode": "duration","duration": "P7D"},{"id": "trial-7d-email","type": "send","channel": "email","message": {"template": "@trial-7d-email"}},{"type": "send","channel": "inbox","message": {"template": "@trial-7d-inbox"}},{"type": "delay","mode": "duration","duration": "P6D"},{"type": "send","channel": "email","message": {"template": "@trial-1d-email"}},{"type": "send","channel": "inbox","message": {"template": "@trial-1d-inbox"}},{"type": "send","channel": "sms","message": {"template": "@trial-1d-sms"},"conditions": {"AND": [["send_status.trial-7d-email", "was not", "OPENED"],["profile.phone_number", "exists"]]}},{"type": "delay","mode": "duration","duration": "P1D"},{"type": "send","channel": "email","message": {"template": "@trial-ended-email"}},{"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/trial-7d-email.json
{"channel": "email","notification": {"name": "First trial reminder (email)","tags": ["recipe", "trial-ending"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Your trial ends on {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "Your {{plan_name}} trial ends on {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}. Pick a plan to keep everything running with no gap."},{"type": "action","content": "Choose a plan","href": "https://app.acme.com/billing"},{"type": "text","content": "Not sure which plan fits? Reply to this email."}]}]}}}
templates/trial-7d-inbox.json
{"channel": "inbox","notification": {"name": "First trial reminder (Inbox)","tags": ["recipe", "trial-ending"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "inbox","elements": [{"type": "meta","title": "Your trial ends on {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}"},{"type": "text","content": "Pick a plan to keep everything running with no gap."},{"type": "action","content": "Choose a plan","href": "https://app.acme.com/billing"}]}]}}}
templates/trial-1d-email.json
{"channel": "email","notification": {"name": "Trial ends tomorrow (email)","tags": ["recipe", "trial-ending"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Your trial ends tomorrow"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "Your {{plan_name}} trial ends tomorrow, {{datetime-format trial_end_date \"MMMM d\" (default profile.timezone \"America/New_York\")}}. Choose a plan today and nothing changes for you or your team."},{"type": "action","content": "Choose a plan","href": "https://app.acme.com/billing"}]}]}}}
templates/trial-1d-inbox.json
{"channel": "inbox","notification": {"name": "Trial ends tomorrow (Inbox)","tags": ["recipe", "trial-ending"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "inbox","elements": [{"type": "meta","title": "Your trial ends tomorrow"},{"type": "text","content": "Choose a plan today to keep your account running."},{"type": "action","content": "Choose a plan","href": "https://app.acme.com/billing"}]}]}}}
templates/trial-1d-sms.json
{"channel": "sms","notification": {"name": "Trial ends tomorrow (SMS)","tags": ["recipe", "trial-ending"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "sms","elements": [{"type": "text","content": "Acme: your trial ends tomorrow. Keep your account running: https://app.acme.com/billing Reply STOP to opt out."}]}]}}}
templates/trial-ended-email.json
{"channel": "email","notification": {"name": "Trial ended (email)","tags": ["recipe", "trial-ending"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Your trial has ended"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "Your {{plan_name}} trial ended today. Everything you set up is saved, so you can pick up where you left off when you choose a plan."},{"type": "action","content": "Choose a plan","href": "https://app.acme.com/billing"}]}]}}}
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 | |
| trial_end_date | string | 2026-10-29T17:00:00Z | An ISO timestamp. The templates format it as a date in the recipient's timezone |
FAQ
Frequently asked questions
How many trial reminder emails should I send?
Two before the trial ends and one when it ends covers most trials. The journey starts when the trial starts and waits P7D before the first reminder, which suits a 14-day trial. For another length, set that first wait to the trial length minus 7 days. To change any other gap, edit the wait durations in the Journeys editor.
How do I stop trial reminders when someone upgrades?
Each run is tagged with the user's ID, so cancel the token trial-<user_id> from your billing webhook. Courier stops every active run for that user, including a run still on its first wait, and leaves finished runs alone.
When does a trial reminder go by SMS?
On step 2, when the first reminder email wasn't opened and the user has a phone number on their profile. Everyone else gets the email and Inbox note without the text.
What if I extend someone's trial?
Cancel the running journey and start a new one. The waits count from the start, so for an extension of a different length, edit the first wait in a copy of the journey. The templates read the new trial_end_date you pass.
Recipes
Related recipes
Hand stuck, high-value trials to a person instead of sending another email.
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.
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.