Account and security

Password reset email: template and code

Send a password reset email with a link that expires, and never deliver a stale one.

Passing, tested 2026-10-19 · Email

How it works

When to send a password reset email

A password reset email sends a link to choose a new password when someone asks for one. It's a single email with one button, plus a line telling anyone who didn't ask that their password hasn't changed. Courier stops trying to deliver it after 30 minutes, so an expired link never arrives.

Messages

What it sends

When someone asks to reset their password

Email.

Email

From: Acme
Reset your Acme password

Someone asked to reset the password for your Acme account. Use the button below to choose a new one. The link expires in 30 minutes.

If you didn't ask for this, you can ignore this email. Your password won't change.

Reset your password

Other subject lines: "Reset your password", "Your Acme password reset link"

Build

Add it to your app

1. Create the template

Run setup.ts once with your API key. It creates the template in your workspace, publishes it, and prints the template ID you pass to step 2. Copy setup.ts 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. Send it when someone asks to reset their password

Node.js

// Send a password reset email with the template ID setup.ts printed. The
// timeout tells Courier to stop trying after the same 30 minutes the reset link
// works for. The template says "30 minutes" too, so change both together.
import Courier from "@trycourier/courier";
const client = new Courier();
const EXPIRES_IN_MINUTES = 30;
export async function sendPasswordReset(
templateId: string,
userId: string,
resetUrl: string
) {
const { requestId } = await client.send.message({
message: {
to: { user_id: userId },
template: templateId,
data: { reset_url: resetUrl },
routing: { method: "single", channels: ["email"] },
timeout: { message: EXPIRES_IN_MINUTES * 60 * 1000 },
},
});
return requestId;
}

Python

# Send a password reset email with the template ID setup.ts printed. The
# timeout tells Courier to stop trying after the same 30 minutes the reset link
# works for. The template says "30 minutes" too, so change both together.
from courier import Courier
client = Courier()
EXPIRES_IN_MINUTES = 30
def send_password_reset(template_id: str, user_id: str, reset_url: str) -> str:
response = client.send.message(
message={
"to": {"user_id": user_id},
"template": template_id,
"data": {"reset_url": reset_url},
"routing": {"method": "single", "channels": ["email"]},
"timeout": {"message": EXPIRES_IN_MINUTES * 60 * 1000},
},
)
return response.request_id
Full definition: setup.ts, and the 1 template

One email template and one send call. Your app creates the reset token and link and passes the link in as reset_url. The send call routes to email with a timeout that matches the link, so Courier stops trying once it no longer works.

Replace the product name (Acme) once, and keep the template's 30 minutes the same as the timeout.

setup.ts

// Creates this recipe's templates in your workspace from the templates/
// folder and publishes them. Run it once per workspace:
//
// COURIER_API_KEY=pk_... node --experimental-strip-types setup.ts
//
// It prints the template IDs that start.ts needs.
import Courier from "@trycourier/courier";
import { readFileSync, readdirSync } from "node:fs";
const client = new Courier();
const here = new URL(".", import.meta.url);
const templateIds: Record<string, string> = {};
for (const file of readdirSync(new URL("templates/", here))) {
const body = JSON.parse(
readFileSync(new URL(`templates/${file}`, here), "utf8")
);
const template = await client.notifications.create(body);
templateIds[file.replace(/\.json$/, "")] = template.id;
}
console.log(JSON.stringify({ templateIds }));

templates/password-reset.json

{
"notification": {
"name": "Password reset",
"tags": ["recipe", "password-reset-email"],
"brand": null,
"subscription": null,
"routing": null,
"content": {
"version": "2022-01-01",
"elements": [
{
"type": "channel",
"channel": "email",
"elements": [
{
"type": "meta",
"title": "Reset your Acme password"
},
{
"type": "text",
"content": "Someone asked to reset the password for your Acme account. Use the button below to choose a new one. The link expires in 30 minutes."
},
{
"type": "action",
"content": "Reset your password",
"href": "{{reset_url}}"
},
{
"type": "text",
"content": "If you didn't ask for this, you can ignore this email. Your password won't change."
}
]
}
]
}
},
"state": "PUBLISHED"
}

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 password reset emails to this app, sent through Courier: one email with a Reset your password button, the link's expiry in the copy, and no retries after the link expires. Follow the steps at https://www.courier.com/recipes/password-reset-email.md and use the Courier skill for SDK details. 1. Create the template in my Courier workspace by running setup.ts from that page with COURIER_API_KEY from our environment. Save the template ID it prints as COURIER_PASSWORD_RESET_TEMPLATE_ID. 2. Find where this codebase creates a password reset token and send the email there with the recipe's send code. Pass only reset_url. Replace the placeholder product name (Acme) in the template with ours, and make its "30 minutes" match the token's real lifetime and the send timeout. 3. Match this codebase's language, file layout, and error handling. Never hardcode the API key. 4. When you're done, request a reset for a test user and show me the rendered email.

Data

Data fields

FieldTypeExampleNote
reset_urlURLhttps://app.acme.com/reset-password?token=8d2f6aYour app creates the link and its token

FAQ

Frequently asked questions

How do I send a password reset email?

Create a reset token in your app, build the reset URL, and pass it to the template as reset_url. The send call delivers the email. The reset page and the token check stay in your app.

What should a password reset email include?

A clear subject line, one button that opens your reset page, how long the link works, and a line saying nothing changes if the person didn't ask. Leave out marketing, extra links, and any password, old or new.

How long should a password reset link be valid?

Long enough to find the email, short enough to limit the risk. Thirty minutes is a reasonable default. Enforce the same limit on the token in your app, and set the message timeout and the minutes in the template to match.

How do I know a password reset email was delivered?

The send call returns a request ID. Look it up with the Messages API to see its status, its delivery history, and the rendered email.

Build this in your workspace

Run setup.ts against a free account and the template is ready to send. Prefer no code? Rebuild it in Design Studio.