# Email verification reminder email: template and code

Source: https://www.courier.com/recipes/email-verification-reminder-email (manifest: https://www.courier.com/recipes/email-verification-reminder-email/manifest.json)

Get new signups to confirm their email, and stop reminding them the moment they do.

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.

Tested 2026-10-19: passing.

The waits are Courier delay nodes. We test this exact journey with each wait shortened to seconds.

## When to send email verification reminders

| Step | When | Channels | Subject |
| --- | --- | --- | --- |
| 1 | Right after signup | Email | Confirm your email for Acme |
| 2 | 1 day after signup | Email | Reminder: confirm your email |
| 3 | 6 days after signup, the day before a 7-day cleanup | Email | Last chance to confirm your email |

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.

## Copy

### Confirm your email (email)

Subject: "Confirm your email for Acme", "One click to confirm your email"

Hi {{default profile.first_name "there"}},

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

Button: Confirm my email (https://app.acme.com/verify?token={{token}})

### Confirm your email reminder (email)

Subject: "Reminder: confirm your email", "You haven't confirmed your email yet"

Hi {{default profile.first_name "there"}},

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

Button: Confirm my email (https://app.acme.com/verify?token={{token}})

### Confirm your email last notice (email)

Subject: "Last chance to confirm your email", "Your account is removed tomorrow unless you confirm"

Hi {{default profile.first_name "there"}},

We remove accounts that haven't confirmed their email, and yours is removed tomorrow. Confirm your email before then and your account stays.

Button: Confirm my email (https://app.acme.com/verify?token={{token}})

## The 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"
    }
  ]
}
```

## Create it in your workspace

```bash
COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
```

setup.ts

```typescript
// 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 }));
```

## Start it when someone signs up

```typescript
// 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
```

## Stop it when they verify

```typescript
// 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}")
```

## Templates

setup.ts reads these from a templates/ folder next to it.

templates/verify-email.json

```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

```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

```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}}"
            }
          ]
        }
      ]
    }
  }
}
```

## Hand it to your agent

```text
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 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

### 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.

## Related docs

- [Delay node](https://www.courier.com/docs/journeys/nodes/delay)
- [Send node conditions](https://www.courier.com/docs/journeys/nodes/send)
- [Cancel a run](https://www.courier.com/docs/journeys/nodes/cancel)
- [Invoke a journey](https://www.courier.com/docs/journeys/invoke)
- [Build an onboarding sequence](https://www.courier.com/docs/guides/build-an-onboarding-sequence)

## Related recipes

- [Welcome email sequence](https://www.courier.com/recipes/welcome-email-sequence): Welcome every new user, then nudge only the ones who haven't finished setup.
- [Magic link email](https://www.courier.com/recipes/magic-link-email): Sign people in with an emailed link, and never deliver one that has already expired.
- [Password reset](https://www.courier.com/recipes/password-reset-email): Send a password reset email with a link that expires, and never deliver a stale one.
