Onboarding
Email verification reminder email: template and code
Get new signups to confirm their email, and stop reminding them the moment they do.
Passing, tested 2026-10-19 · Email
How it works
When to send email verification reminders
An email verification reminder gets new signups to confirm their email address. It sends the verification email at signup, a reminder the next day, and a last notice on day 6, the day before unverified accounts are removed. Everything goes by email, and confirming the address stops the reminders.
- SignupYour app starts the journey
- Right awayVerification email
- Day 1Reminder emailCanceled once the email is verified
- Day 6Last notice by emailCanceled once the email is verified
Stops early: When the email is verified, cancel the run and every reminder still to come is dropped. Cancel it the same way when the account is deleted.
The waits are Courier delay nodes. We test this exact journey with each wait shortened to seconds.
Messages
What each step sends
Step 1: Right after signup
Verification email.
Hi Dana,
Welcome to Acme. Confirm your email address to finish setting up your account.
Other subject lines: "One click to confirm your email"
Step 2: 1 day after signup
Reminder email. Canceled once the email is verified.
Hi Dana,
Your Acme account is waiting on one thing: confirming your email address.
Other subject lines: "You haven't confirmed your email yet"
Step 3: 6 days after signup, the day before a 7-day cleanup
Last notice by email. Canceled once the email is verified.
Hi Dana,
We remove accounts that haven't confirmed their email, and yours is removed tomorrow. Confirm your email before then and your account stays.
Other subject lines: "Your account is removed tomorrow unless you confirm"
This recipe assumes you remove unverified accounts after 7 days, so the last notice goes on day 6. If your window is different, change the second wait in the journey, and change "tomorrow" in the last notice to match.
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 someone signs up
Node.js
// Start the reminders when someone signs up, with the journey ID setup.ts printed.// The last notice goes out on day 6 and says the account is removed tomorrow,// so this assumes you remove unverified accounts after 7 days. The templates// build the verify link from the token.import Courier from "@trycourier/courier";const client = new Courier();export async function startEmailVerification(journeyId: string,userId: string,verificationToken: string // the token your app already issued for this user) {const { runId } = await client.journeys.invoke(journeyId, {user_id: userId,data: { token: verificationToken },});return runId;}
Python
# Start the reminders when someone signs up, with the journey ID setup.ts printed.# The last notice goes out on day 6 and says the account is removed tomorrow,# so this assumes you remove unverified accounts after 7 days. The templates# build the verify link from the token.from courier import Courierclient = Courier()def start_email_verification(journey_id: str, user_id: str, verification_token: str) -> str:# verification_token is the token your app already issued for this user.response = client.journeys.invoke(template_id=journey_id,user_id=user_id,data={"token": verification_token},)return response.run_id
3. Stop it when they verify
Node.js
// Stop the reminders when the email is verified, or when the account is deleted.// It cancels every run waiting for the user, and does nothing if the sequence// already finished.import Courier from "@trycourier/courier";const client = new Courier();export async function stopEmailVerification(userId: string) {await client.journeys.cancel({ cancelation_token: `verify-${userId}` });}
Python
# Stop the reminders when the email is verified, or when the account is deleted.# It cancels every run waiting for the user, and does nothing if the sequence# already finished.from courier import Courierclient = Courier()def stop_email_verification(user_id: str) -> None:client.journeys.cancel(cancelation_token=f"verify-{user_id}")
Full definition: journey.json, setup.ts, and the 3 templates
The verification email goes out right away, a reminder follows a day later, and a last notice goes out on day 6 saying the account is removed tomorrow.
There are no conditions. When the email is verified, your app cancels the run and every send still to come is dropped.
journey.json
{"name": "Email verification reminders","cancelation_token": "verify-{{recipient}}","nodes": [{"type": "trigger","trigger_type": "api-invoke"},{"type": "send","channel": "email","message": {"template": "@verify-email"}},{"type": "delay","mode": "duration","duration": "P1D"},{"type": "send","channel": "email","message": {"template": "@verify-reminder-email"}},{"type": "delay","mode": "duration","duration": "P5D"},{"type": "send","channel": "email","message": {"template": "@verify-final-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/verify-email.json
{"channel": "email","notification": {"name": "Confirm your email (email)","tags": ["recipe", "email-verification-reminder-email"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Confirm your email for Acme"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "Welcome to Acme. Confirm your email address to finish setting up your account."},{"type": "action","content": "Confirm my email","href": "https://app.acme.com/verify?token={{token}}"}]}]}}}
templates/verify-reminder-email.json
{"channel": "email","notification": {"name": "Confirm your email reminder (email)","tags": ["recipe", "email-verification-reminder-email"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Reminder: confirm your email"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "Your Acme account is waiting on one thing: confirming your email address."},{"type": "action","content": "Confirm my email","href": "https://app.acme.com/verify?token={{token}}"}]}]}}}
templates/verify-final-email.json
{"channel": "email","notification": {"name": "Confirm your email last notice (email)","tags": ["recipe", "email-verification-reminder-email"],"brand": null,"subscription": null,"content": {"version": "2022-01-01","elements": [{"type": "channel","channel": "email","elements": [{"type": "meta","title": "Last chance to confirm your email"},{"type": "text","content": "Hi {{default profile.first_name \"there\"}},"},{"type": "text","content": "We remove accounts that haven't confirmed their email, and yours is removed tomorrow. Confirm your email before then and your account stays."},{"type": "action","content": "Confirm my email","href": "https://app.acme.com/verify?token={{token}}"}]}]}}}
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 |
| token | string | k3J9x | The verification token your app already issued, not the user ID. The templates build the link from it, so use URL-safe characters |
FAQ
Frequently asked questions
How many email verification reminders should I send?
One reminder the next day and one last notice before cleanup is usually enough. More than that starts to feel like nagging.
How do I stop sending reminders once someone verifies?
When the email is confirmed, cancel the run with the token verify-<user_id>. That drops the reminder and the last notice, and nothing is stored on the profile.
How long should I wait before removing unverified accounts?
That's your call. This recipe assumes 7 days and sends the last notice on day 6. If your window is different, change the second wait in the journey, and change "tomorrow" in the last notice email to match.
Which address do the verification reminders go to?
The email on the user's Courier profile, so make sure it's set before you start the journey.
Recipes
Related recipes
Docs
How each part works
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.