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": {
    "records": [
      { "type": "delivered", "recipient": "user@corp.example", "occurred_at": "2026-07-26T10:02:11Z" },
      { "type": "clicked", "recipient": "user@corp.example", "occurred_at": "2026-07-26T10:02:13Z" }
    ],
    "total_count": 2,
    "next_cursor": null
  }
}

Two seconds between delivery and click is not a human. Worth flagging as a limitation on our side: there’s no per-message switch to disable link tracking today, so the 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.

SituationDaily volumeWhat to do
Normal resetsTens per dayNothing; the cap is far above you
Product launch spikeHundreds per dayWatch used_today against current_daily_cap
Forced reset, new domainTens of thousandsStagger over days, or send from an established domain
Forced reset, established domainTens of thousandsUse 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.000115 per recipient, verified 2026-07-26; a 40,000-address forced reset is about $4.60. 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 in credit, roughly 17,391 messages. Rates in this market drift down as vendor discounts land, so 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 (HTTP 402 PRO_REQUIRED otherwise); and there’s no per-send tracking toggle. If reset latency and inbox placement are your single most important metric, Postmark’s transactional-only stance is a defensible reason to stick with a specialist. Amazon SES is cheaper per message if you’re willing to own the bounce plumbing.

Where consolidation pays here is the rest of the incident: the audit log entry, the queue that staggers 40,000 sends, the error event when one fails, the per-tenant cost line — same key, same bill, no second vendor to page.

References

Browse more email developer guides