Here's the full flow the way I've built it, using Notify as the email API. The shape of this is the same regardless of which provider you pick — generate a token, send a link, verify it on submit — so most of this applies no matter what you're using; I'll flag the one part that's specific to Notify.
The Flow, End to End
- User requests a password reset
- Your backend generates a secure, short-lived reset token
- Your backend stores a hashed version of that token
- Your backend sends an email with the reset link, through an email API
- User clicks the link and submits a new password
- Your backend verifies the token, updates the password, and invalidates the token
Step 1: Generate the Reset Token
Use a cryptographically secure random value, not anything guessable, and store only a hashed version in your database — if your database ever leaks, the raw tokens aren't exposed alongside it:
const crypto = require('crypto');
function generateResetToken() {
const token = crypto.randomBytes(32).toString('hex');
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
return { token, tokenHash };
}
Give it a short expiration — 15 to 60 minutes is typical.
Step 2: Build the Reset URL
https://yourapp.com/reset-password?token=RESET_TOKEN
The token goes in the link the user clicks; the hash is what you store and check against later.
Step 3: Send the Email
This is the Notify-specific part. There's no SDK to install — it's a single HTTP request with your API key in the header:
async function requestPasswordReset(email) {
const user = await findUserByEmail(email);
// Don't reveal whether the email exists
if (!user) return;
const { token, tokenHash } = generateResetToken();
const expiresAt = new Date(Date.now() + 1000 * 60 * 30); // 30 minutes
await saveResetToken(user.id, tokenHash, expiresAt);
const resetLink = `https://yourapp.com/reset-password?token=${token}`;
await fetch('https://notify.cx/api/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.NOTIFY_API_KEY
},
body: JSON.stringify({
to: email,
from: 'noreply@your-verified-domain.com',
subject: 'Reset your password',
message: `<p>Click below to reset your password. This link expires in 30 minutes.</p><p><a href="${resetLink}">Reset password</a></p>`
})
});
}
If you want to catch a bounce automatically — say the address was mistyped at signup — register a webhook once, and your app finds out without a support ticket:
await fetch('https://notify.cx/api/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.NOTIFY_API_KEY
},
body: JSON.stringify({
webhookUrl: 'https://yourapp.com/webhooks/email',
subscribedEvents: ['Bounce'],
domainId: 'your-domain-id'
})
});
If you want the full request/response shape and event list before wiring this in, the docs cover both in a few minutes.
Step 4: Verify the Token on Reset
This part is entirely your application logic, not the email provider's concern:
async function resetPassword(token, newPassword) {
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const record = await findResetToken(tokenHash);
if (!record) {
throw new Error('Invalid token');
}
if (new Date() > record.expiresAt) {
throw new Error('Token expired');
}
const passwordHash = await hashPassword(newPassword); // bcrypt or argon2
await updateUserPassword(record.userId, passwordHash);
await deleteResetToken(tokenHash); // one-time use
}
Security Practices Worth Following, Regardless of Provider
- Use HTTPS everywhere in this flow
- Make tokens random and single-use
- Set short expiration times
- Store only hashed tokens, never the raw value
- Rate-limit reset requests to prevent abuse
- Don't reveal whether an email address exists in your system
- Invalidate active sessions after a successful reset
- Use bcrypt or Argon2 for the new password hash — never store it in plain text
- Never put the user's actual password in an email — only a reset link or one-time code
Why I Used Notify Here
None of the token logic above changes based on which email API sends the message — that's the point of separating the two concerns. What differs is what's required to get to that fetch call: with Notify, that's a verified domain and an API key, with no SDK to install and no template system to learn since you're building the HTML directly, same as above. I've found the free tier is enough to build and test this entire flow before deciding it's worth paying for.
Frequently Asked Questions
How do I send password reset emails from a backend app using an email API?
Generate a random token, store a hashed version with an expiration, send an email containing the reset link through an email API like Notify, and verify the token when the user submits a new password. Notify's part of this is a single authenticated HTTP request — POST https://notify.cx/api/email/send — with no SDK or template system required.
Should I send the actual password in the reset email?
No — only send a reset link or one-time code. The email itself should never contain the user's password, current or new.
How long should a password reset token stay valid?
15 to 60 minutes is typical. Shorter is safer; just make sure it's long enough that a user checking their email a few minutes late doesn't hit a dead link.
Do I need a separate library to send email through Notify?
No — Notify doesn't have an SDK. You send a plain HTTP request with your API key in the x-api-key header, which works the same way in Node.js, Python, or any language with an HTTP client.
How do I know if a password reset email failed to deliver?
Register a webhook subscribed to the Bounce event on your domain, and Notify will notify your app automatically instead of the user having to report it.
Top comments (0)