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.

  1. Signup
    Your app starts the journey
  2. Right away
    Verification email
  3. Day 1
    Reminder email
    Canceled once the email is verified
  4. Day 6
    Last notice by email
    Canceled 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.

Email

From: Acme
Confirm your email for Acme

Hi Dana,

Welcome to Acme. Confirm your email address to finish setting up your account.

Confirm my email

Other subject lines: "One click to confirm your email"

Step 2: 1 day after signup

Reminder email. Canceled once the email is verified.

Email

From: Acme
Reminder: confirm your email

Hi Dana,

Your Acme account is waiting on one thing: confirming your email address.

Confirm my email

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.

Email

From: Acme
Last chance to confirm your email

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.

Confirm my email

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 Courier
client = 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 Courier
client = 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.

Add email verification reminders to this app, sent through Courier: a verification email at signup, a reminder a day later, and a last notice the day before we clean up unverified accounts, all skipped once the email is verified. Follow the steps at https://www.courier.com/recipes/email-verification-reminder-email.md and use the Courier skill for SDK details. 1. Create the journey and its templates in my Courier workspace by replacing Acme and the verification link in the template files with our own, then running setup.ts from that page with COURIER_API_KEY from our environment. The last notice goes on day 6, so if we remove unverified accounts after something other than 7 days, change the second wait in journey.json first. Save the journey ID it prints as COURIER_VERIFY_JOURNEY_ID. 2. Find where this codebase creates a new user and issues their email verification token, and start the journey there. Pass the fields in the Data fields table: token is the verification token we already issue, never the user ID. Replace the base URL in the templates with our verification page. 3. Find where an email address gets verified and cancel the journey there with the token verify-<user_id>. Cancel it the same way when an account is deleted. 4. Match this codebase's language, file layout, and error handling. Never hardcode the API key. 5. When you're done, start one run for a test user and show me the run's steps.

Data

Data fields

FieldTypeExampleNote
profile.first_namestringDanaFrom the profile. "there" is the fallback when it's missing
tokenstringk3J9xThe 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.

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.