> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lettr.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Password Reset Emails

> Best practices for secure, deliverable password reset emails — timing, token expiry, content, and implementation

Password reset emails are the most common transactional email type and one of the most security-sensitive. A poorly implemented reset flow can lock users out of their accounts, expose tokens to attackers, or land in spam at the worst possible moment. This guide covers the full lifecycle — from generating the reset link to delivering it reliably.

***

## Why Password Resets Are Unique

Password reset emails differ from other transactional emails in several ways:

| Factor                    | Impact                                                                                                                    |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Time-sensitive**        | Users expect delivery within seconds, not minutes                                                                         |
| **Security-critical**     | The email contains a credential-equivalent token                                                                          |
| **High-engagement**       | Users actively check their inbox, so deliverability is rarely a perception problem — but failures are immediately noticed |
| **Single-use**            | The token should only work once and expire quickly                                                                        |
| **Triggered by the user** | The recipient explicitly requested this email, so spam complaints are rare                                                |

***

## Token Security

The reset token is the most important part of the email. A weak or poorly managed token can let attackers take over accounts.

### Generate Cryptographically Secure Tokens

Use your language's cryptographically secure random generator — never `Math.random()` or similar predictable sources:

```javascript theme={null}
const crypto = require('crypto');

// Generate a 32-byte (256-bit) token
const token = crypto.randomBytes(32).toString('hex');
// Example: "a3f8c1d9e4b2...64 hex characters"
```

```php theme={null}
// Laravel — built-in password broker handles this
// If generating manually:
$token = bin2hex(random_bytes(32));
```

### Set Short Expiry Times

Reset tokens should expire quickly. The user just requested the email, so they're actively waiting for it:

| Expiry            | Recommendation                                         |
| ----------------- | ------------------------------------------------------ |
| **15–60 minutes** | Recommended range                                      |
| **30 minutes**    | Good default                                           |
| **24 hours**      | Too long — increases the window for token interception |

```javascript theme={null}
const resetExpiry = new Date(Date.now() + 30 * 60 * 1000); // 30 minutes

await db.passwordResets.create({
  email: user.email,
  token: hashedToken,
  expiresAt: resetExpiry,
});
```

<Warning>
  Store a **hashed** version of the token in your database, not the raw token. If your database is compromised, raw tokens would allow attackers to reset any pending account.
</Warning>

### Invalidate Tokens After Use

A token should become invalid the moment it's used. Also invalidate any existing tokens when a new reset is requested:

```javascript theme={null}
async function handlePasswordReset(token, newPassword) {
  const reset = await db.passwordResets.findByToken(hash(token));

  if (!reset || reset.expiresAt < new Date()) {
    throw new Error('Invalid or expired reset link');
  }

  await db.users.updatePassword(reset.email, newPassword);
  await db.passwordResets.deleteAllForEmail(reset.email);
  // All previous tokens for this user are now invalid
}
```

***

## Email Content

### Keep the Email Simple

A password reset email should contain exactly what the user needs and nothing more:

<Steps>
  <Step title="Acknowledge the request">
    A brief statement confirming that a password reset was requested for their account.
  </Step>

  <Step title="Provide the reset link">
    A clear, prominent button or link to reset their password.
  </Step>

  <Step title="State the expiry">
    Tell the user how long the link is valid so they know to act promptly.
  </Step>

  <Step title="Include a safety notice">
    Let them know to ignore the email if they didn't request it.
  </Step>
</Steps>

### Example HTML Structure

```html theme={null}
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto;">
  <h2 style="color: #1a1a1a;">Reset your password</h2>

  <p style="color: #4a4a4a; line-height: 1.6;">
    We received a request to reset the password for your account
    associated with {{email}}.
  </p>

  <div style="text-align: center; margin: 32px 0;">
    <a href="{{resetUrl}}"
       style="background-color: #6366F1; color: #ffffff; padding: 12px 32px;
              text-decoration: none; border-radius: 6px; font-weight: 600;">
      Reset Password
    </a>
  </div>

  <p style="color: #6b6b6b; font-size: 14px; line-height: 1.6;">
    This link expires in 30 minutes. If you didn't request a password reset,
    you can safely ignore this email — your password won't be changed.
  </p>

  <p style="color: #6b6b6b; font-size: 14px; line-height: 1.6;">
    If the button doesn't work, copy and paste this URL into your browser:<br>
    <a href="{{resetUrl}}" style="color: #6366F1; word-break: break-all;">{{resetUrl}}</a>
  </p>
</div>
```

<Tip>
  Always include a plain-text fallback URL below the button. Some email clients block images and styled buttons, and some users prefer to inspect the URL before clicking.
</Tip>

### What Not to Include

* **Don't include the user's password** — not even a temporary one. Always use a link to a secure reset form.
* **Don't include the username** — if the email is intercepted, this reveals the account identifier.
* **Don't include marketing content** — this is a transactional email. Adding promotions risks spam classification and violates CAN-SPAM regulations.
* **Don't include account details** beyond the email address — minimize information exposure.

***

## Sending with Lettr

```javascript theme={null}
const response = await fetch('https://app.lettr.com/api/emails', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LETTR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'security@mail.yourdomain.com',
    to: user.email,
    subject: 'Reset your password',
    html: resetEmailHtml,
    text: resetEmailText,
    tags: ['password-reset'],
    metadata: {
      userId: user.id,
      resetRequestId: requestId,
    },
  }),
});
```

<Note>
  Use a `from` address on a subdomain (like `mail.yourdomain.com`) rather than your root domain. This isolates your transactional sending reputation from any marketing email you may send. See [Subdomain vs Root Domain](/knowledge-base/fundamentals/subdomain-vs-root) for details.
</Note>

***

## Delivery Speed

Password resets are one of the few email types where delivery speed is immediately noticeable. Users are staring at their inbox waiting.

### Optimize for Fast Delivery

* **Send immediately** — don't queue password reset emails behind bulk sends. Use a separate sending priority or queue if your system batches emails.
* **Keep the email small** — avoid large images, heavy templates, or excessive HTML. A lightweight email processes faster at every stage of the delivery pipeline.
* **Use a reputable sending domain** — domains with established reputation are processed faster by receiving servers. See [Sending Reputation](/knowledge-base/best-practices/sending-reputation).
* **Monitor delivery time** — use Lettr webhooks to track the time between your API call and the `email.delivered` event. Alert if this exceeds a few seconds.

```javascript theme={null}
// Track delivery latency via webhooks
app.post('/webhooks/lettr', express.raw({ type: 'application/json' }), (req, res) => {
  const event = JSON.parse(req.body);

  if (event.type === 'email.delivered' && event.data.tags?.includes('password-reset')) {
    const latency = new Date(event.data.deliveredAt) - new Date(event.data.sentAt);
    metrics.recordDeliveryLatency('password-reset', latency);

    if (latency > 10000) { // More than 10 seconds
      alertOps('Slow password reset delivery detected');
    }
  }

  res.sendStatus(200);
});
```

***

## Handling Edge Cases

### User Doesn't Receive the Email

Provide a "Resend" button on your password reset page, but rate-limit it:

* Allow a maximum of **3 reset emails per email address per hour**
* Show a generic message regardless of whether the email exists in your system (prevents account enumeration)

```javascript theme={null}
// Don't reveal whether the email exists
app.post('/api/password-reset', async (req, res) => {
  const { email } = req.body;

  // Always return the same response
  res.json({ message: 'If an account exists with that email, a reset link has been sent.' });

  // Process in the background
  const user = await db.users.findByEmail(email);
  if (user && !isRateLimited(email)) {
    await sendPasswordResetEmail(user);
  }
});
```

### Expired Token

When a user clicks an expired link, show a clear message and offer to send a new one:

* Don't just say "invalid link" — tell them the link has expired
* Provide a button to request a new reset email
* Don't auto-send a new email without user action

### Reset from an Unrecognized Device

Consider sending a follow-up notification after a successful password change:

```javascript theme={null}
// After password is successfully changed
await fetch('https://app.lettr.com/api/emails', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.LETTR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'security@mail.yourdomain.com',
    to: user.email,
    subject: 'Your password was changed',
    html: passwordChangedHtml,
    text: passwordChangedText,
    tags: ['password-changed'],
  }),
});
```

This alerts the user if someone else reset their password, giving them a chance to act.

***

## Common Mistakes

<AccordionGroup>
  <Accordion title="Using predictable or sequential tokens">
    Tokens generated with `Math.random()`, auto-incrementing IDs, or timestamps are guessable. Always use `crypto.randomBytes()` or your language's equivalent CSPRNG.
  </Accordion>

  <Accordion title="Not hashing stored tokens">
    If you store raw tokens in your database, a database breach exposes all pending resets. Hash tokens before storage using SHA-256 or bcrypt.
  </Accordion>

  <Accordion title="Token doesn't expire or expires too late">
    A 24-hour or never-expiring token gives attackers a large window. Use 30–60 minutes as the default.
  </Accordion>

  <Accordion title="Sending the password in the email">
    Never email a password — temporary or otherwise. Always send a link to a form where the user sets a new password over HTTPS.
  </Accordion>

  <Accordion title="Revealing account existence">
    If your reset endpoint returns different responses for existing vs non-existing emails, attackers can enumerate your user base. Always return the same message.
  </Accordion>

  <Accordion title="Adding marketing content to the email">
    Password reset emails are transactional. Adding promotional content can trigger spam filters and may violate CAN-SPAM. Keep the email focused.
  </Accordion>
</AccordionGroup>

***

## Related Topics

<CardGroup cols={2}>
  <Card title="Two-Factor Authentication Emails" icon="lock" href="/knowledge-base/use-cases/two-factor-authentication-emails">
    OTP delivery best practices and security considerations
  </Card>

  <Card title="Security Best Practices" icon="shield-check" href="/knowledge-base/best-practices/security">
    Protect your API keys, webhooks, and account
  </Card>

  <Card title="Deliverability Best Practices" icon="inbox" href="/knowledge-base/best-practices/deliverability">
    Maximize inbox placement for your emails
  </Card>

  <Card title="Transactional vs Marketing" icon="arrows-split-up-and-left" href="/knowledge-base/best-practices/transactional-vs-marketing">
    Why separation matters for deliverability
  </Card>
</CardGroup>
