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.
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.
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 Courierclient = Courier()EXPIRES_IN_MINUTES = 30def 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.
Data
Data fields
| Field | Type | Example | Note |
|---|---|---|---|
| reset_url | URL | https://app.acme.com/reset-password?token=8d2f6a | Your 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.
Recipes
Related recipes
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.