Password reset email: arriving in 60 seconds, and not burning the token
Reset mail is the message with a deadline. Sender setup, why scanners consume single-use links, warm-up caps during a forced reset, and a Node 22 handler.
A password reset is the only email your product sends where the user is watching the inbox with a stopwatch. Spam-foldering a newsletter costs you an open rate; spam-foldering a reset costs you the account and generates a support ticket. So the setup bar is higher: an authenticated sender the receiver trusts, a token design that survives being fetched by a security scanner, and enough headroom in your daily cap for the day you have to reset everyone. Infrai’s email routes cover the first and third; the second is your code, and it’s the part most guides skip.
We’ll go through the sender first, then the token, then what changes during a mass reset.
Sender setup, in the order that matters
Publish SPF, DKIM and DMARC on the subdomain the resets come from, and don’t share that subdomain with anything promotional. Resend’s authentication guide is a good primer on the three records if you want one; the operational point is that DMARC only passes when a passing identifier aligns with the visible From domain, which is why “we have SPF” and “our mail authenticates” aren’t the same statement.
Registration hands you the exact values:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS -X POST "https://api.infrai.cc/v1/email/domain/verify" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"domain":"auth.example.com"}'
Publish, call again until status is verified, then read the reputation side before you route live traffic to it:
curl -sS "https://api.infrai.cc/v1/email/domain/get/auth.example.com" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"verification": {
"status": "verified",
"checks": { "spf_dns": "verified", "dkim_dns": "verified", "dmarc_dns": "verified", "mail_loopback": "verified" }
},
"reputation": {
"tier": "warming_up",
"current_daily_cap": 500,
"used_today": 41,
"bounce_rate_30d": 0.004,
"complaint_rate_30d": 0.0,
"throttle_risk": "low"
}
}
}
mail_loopback is the check worth trusting most — it means a real message went out and came back, rather than a DNS record merely existing.
The token problem nobody warns beginners about
Corporate mail security fetches links before the user does. Microsoft’s Safe Links, Proofpoint’s URL Defense and several gateway products expand and follow URLs in inbound mail to see where they land, and they do it within seconds of delivery. If your reset link is single-use and consumed on GET, the scanner burns it, and your customer clicks a dead link and files a ticket saying the reset is broken.
That failure is invisible in your own logs unless you look for it, because from your side the token was used successfully.
Three rules make it survivable. Never consume a token on GET — serve a form and require a POST with a CSRF token to actually change the password. Give the token a real TTL, 15 to 30 minutes, and store only a hash of it. And when you’re reading events, treat a clicked a second or two after delivered, from a user who never reaches your form, as a scanner rather than engagement.
curl -sS "https://api.infrai.cc/v1/email/event/list?message_id=msg_Pd9wKt2LhQ4nRsXcAv&limit=20" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"items": [
{ "type": "delivered", "recipient": "user@corp.example", "at": "2026-07-26T10:02:11Z" },
{ "type": "clicked", "recipient": "user@corp.example", "at": "2026-07-26T10:02:13Z" }
],
"next_cursor": null,
"count": 2
}
}
Two seconds between delivery and click is not a human. The array is items and the timestamp is at — worth pinning in your parser, because both names differ from what several other providers’ SDKs hand you.
Click tracking is a per-send decision: track_clicks on POST /v1/email/send defaults to false, so a reset link goes out exactly as you wrote it unless you opt in. Leave it off for reset mail. The catch is that opting out changes nothing about the recipient’s own gateway, which will still fetch the URL, so the real defence lives in your token semantics rather than in a request field.
The handler, in Node 22
import { randomBytes, createHash } from "node:crypto";
import process from "node:process";
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const TTL_MINUTES = 20;
const FROM = "security@auth.example.com";
async function call(path, init = {}) {
const res = await fetch(API + path, {
...init,
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
});
const payload = await res.json().catch(() => ({}));
if (!res.ok || payload.ok === false) {
const err = payload.error ?? { code: `HTTP_${res.status}`, message: res.statusText };
throw new Error(`${path} -> ${err.code}: ${err.message}`);
}
return payload.data;
}
/** `store` is your database: save({emailHash, tokenHash, expiresAt}). */
export async function requestReset(email, store) {
const token = randomBytes(32).toString("base64url");
const tokenHash = createHash("sha256").update(token).digest("hex");
const expiresAt = new Date(Date.now() + TTL_MINUTES * 60_000);
const screen = await call(`/v1/email/suppression/check/${encodeURIComponent(email)}`);
if (screen.suppressed) {
await store.flagForSupport({ email, reason: screen.reason ?? "suppressed" });
return { sent: false, reason: "suppressed" };
}
await store.save({ email, tokenHash, expiresAt });
const link = `https://app.example.com/reset?token=${token}`;
const sent = await call("/v1/email/send", {
method: "POST",
body: JSON.stringify({
to: email,
from: FROM,
subject: "Reset your Acme password",
html: `<p>Open this link within ${TTL_MINUTES} minutes to choose a new password.</p>
<p><a href="${link}">Reset your password</a></p>
<p>If you didn't ask for this, ignore the message — nothing has changed.</p>`,
}),
});
await store.recordDelivery({ email, messageId: sent.message_id });
return { sent: sent.accepted_recipients.length > 0, messageId: sent.message_id };
}
const outcome = await requestReset("user@corp.example", {
save: async () => {},
recordDelivery: async () => {},
flagForSupport: async () => {},
});
console.log(outcome);
The suppression check before the send is the piece that turns a lockout into a support workflow. A user whose address hard-bounced six months ago is permanently blocked, and if you just return the usual “if that account exists, we’ve sent a link” they will keep retrying forever. Log it, alert support, and let a human intervene — while still showing the generic message to the browser, because you don’t want the endpoint confirming which addresses exist.
Store message_id against the reset attempt. When the ticket arrives you answer it in one call:
curl -sS "https://api.infrai.cc/v1/email/get/msg_Pd9wKt2LhQ4nRsXcAv" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
A state of bounced ends the conversation immediately. An id from the wrong environment comes back as EMAIL_NOT_FOUND, which is usually a staging id pasted into a production query.
The forced-reset day
Warm-up caps are irrelevant for normal reset volume — a few dozen a day fits inside a new domain’s 500 comfortably. They stop being irrelevant the moment security asks you to invalidate every session and email 40,000 people.
| Situation | Daily volume | What to do |
|---|---|---|
| Normal resets | Tens per day | Nothing; the cap is far above you |
| Product launch spike | Hundreds per day | Watch used_today against current_daily_cap |
| Forced reset, new domain | Tens of thousands | Stagger over days, or send from an established domain |
| Forced reset, established domain | Tens of thousands | Use POST /v1/email/batch/send, and page the events afterwards |
Read the cap before you start, not after the first 500 messages silently stop:
curl -sS "https://api.infrai.cc/v1/email/domain/list" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Cost, and the trade-offs
Sends are $0.00046 per recipient, verified 2026-07-27 and published as approximate. Domain reads, suppression, message and event lookups are free and rate-limited, so the monitoring around a reset flow costs nothing to run. New accounts start with $2 of free credit. Multiply the rate by your own reset population rather than trusting a worked total in a paragraph — this rate card changed recently, and every article that had done the multiplication became wrong the same day. Check rather than quote:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(next(c['billing'] for c in d['capabilities'] if c['id']=='email.send'))"
The honest boundaries: no outbound webhooks, so a bounce reaches you when you poll for it; custom sender domains need a paid plan, declared as minimum_tier on the capability and answered as HTTP 402 PRO_REQUIRED on a standard key; and warm-up caps are set by reputation rather than by request. If reset latency and inbox placement are the metric your whole business runs on, stick with a specialist — buy Postmark if you want their transactional-only stance and a support engineer who reads DMARC aggregates with you. Buy SES instead if volume is enormous and you’re happy owning the bounce plumbing yourself.
Where consolidation pays here is the rest of the incident, and it is worth naming the routes rather than waving at breadth. The staggering job is POST /v1/queue/publish; the audit trail is POST /v1/logs/ingest; the send that failed mid-reset lands in POST /v1/errors/capture; the tenant you have to bill for it comes out of GET /v1/account/usage. All four answer to the key that just verified your sending domain, so a forced-reset runbook needs no second account and no second vendor paged at 3am.