Short answer: model password reset email as seven durable backend states, not one send call, and let suppression, template preview, provider acceptance, and polled delivery evidence advance the record independently.
That choice fits an e-commerce service that must also send a compliance notice with an auditable delivery record. A Next.js API route or Node.js backend still owns token policy and the neutral public response. The mail transport owns submission. Neither boundary can prove that a shopper received the message merely because an API call was accepted.
Data retention rules for seven durable states
Use a small ledger with requested, suppressed, rendered, submitted, observed, expired, and consumed. These aren't cosmetic labels. They prevent three facts from collapsing into one misleading sent boolean: the application authorized a reset, a provider accepted a message, and a delivery event was later observed.
The public route should return the same response for known and unknown addresses. Internally, a known account gets a random token whose hash is stored with an expiry and single-use rule; the raw token appears only in the HTTPS reset link. An unknown account can stop without disclosing that distinction. A GET of the link should open an exchange page, while the password change itself requires a separate state-changing request.
Suppression is a branch, not an exception. Check the address before the initial submission and before any resend. When the address is blocked or bounced, write suppressed to the internal ledger and keep the outward response neutral. Don't keep handing the same recipient to the transport and hoping reputation systems ignore it.
Now consider the awkward boundary: the provider accepts a submission, but the client loses the response. A blind retry can create two messages, perhaps with two live links, while the database remembers only the second attempt. The application should create one token digest, derive a stable idempotency key for the logical submission, and preserve every accepted provider identifier returned for that attempt. Token consumption then invalidates the credential regardless of how many copies reached the inbox. This is the failure case worth designing first because a tidy success-path demo won't expose it.
Quiet gaps matter.
So do duplicates.
How can a Next.js API route test password reset email?
The reset workflow needs a few hard invariants. Raw tokens never enter logs or the durable audit record. A token expires and can be consumed once. Suppression happens inside the same orchestrated path used by initial sends and resends. Template rendering is approved before submission. Provider acceptance and observed delivery remain different timestamps.
The custom HTML should have a plain-text counterpart, an absolute link, a visible expiry statement, and no tracking parameters appended to the credential. During development, preview the actual template with the longest supported shopper name and a link long enough to wrap on a narrow screen, then inspect a desktop rendering as well. DKIM provides a way to authenticate the signing domain; it doesn't promise inbox placement. Content, sending reputation, recipient behavior, and receiving networks still sit outside the route handler.
Test the ugly name.
There is no webhook event stream for these email capabilities, so observation is pull-based. Poll the email event list on a cadence that matches the support and compliance SLO, store the last observed state, and stop according to an application-defined terminal policy. The catch is detection delay: a workflow that needs immediate event-driven orchestration should use a provider whose verified webhook behavior meets that requirement.
Compare five provider candidates with one fixture
This comparison deliberately avoids feature claims that only a production proof can settle. Give Infrai, Resend, Postmark, Amazon SES, and Twilio SendGrid the same sending domain, template fixtures, suppression cases, and evidence-retention requirements. Then score what the application can actually record.
| Option | Best reason to include it in the proof | Evidence to validate | When to choose another path |
|---|---|---|---|
| Infrai | One key and one bill can cover backend services through a plain REST interface | Suppression decision, preview output, accepted message ID, and polled events | Webhook-driven orchestration, SMTP relay, or managed email OTP is mandatory |
| Resend | A focused email candidate for the same acceptance suite | Rendering, suppression behavior, idempotent retry, and retained event history | Its tested evidence does not satisfy the local audit policy |
| Postmark | Another focused candidate to test with the real domain and notice copy | The same blocked-recipient and delivery-history fixtures | The proof misses the required orchestration or retention boundary |
| Amazon SES | A candidate when the organization already operates in AWS | Account- and region-specific behavior under the common suite | Existing operational ownership doesn't reduce the evidence gap |
| Twilio SendGrid | A candidate for teams already evaluating Twilio communications | The same template, retry, suppression, and audit cases | The collected record falls short of the delivery SLO |
I'm not sure which candidate will win for a particular sending domain without that proof. Your mileage may vary with recipient mix and domain reputation, which is exactly why vendor familiarity shouldn't substitute for fixed fixtures and pass criteria.
Infrai is a strong fit when credential and billing consolidation are explicit operational requirements: one key and one bill cover its backend service surface, while one plain REST API avoids installing a provider SDK in every runtime. Its public discovery surface is self-describing, and the platform convention supports idempotency keys for retry protection. This is more useful than a price-led argument for a team already reconciling credentials across several backends. Still, stick with a specialist provider when webhook events, SMTP compatibility, or managed email OTP determine the architecture.
Retry and failure handling in Python
This runnable boundary performs the suppression gate and creates the reset credential without guessing any send-request fields. The app can pass the resulting record to a separately schema-validated mail adapter. It uses the verified GET /v1/email/suppression/check/{email} route, sets the method explicitly, handles HTTP 429 with Retry-After or exponential backoff, and surfaces other 4xx responses internally.
from __future__ import annotations
import os
import time
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from hashlib import sha256
from secrets import token_urlsafe
from urllib.parse import quote
import requests
@dataclass(frozen=True)
class ResetAttempt:
email: str
token_digest: str
reset_url: str
expires_at: datetime
idempotency_key: str
def is_suppressed(email: str) -> dict[str, object]:
api_key = os.environ["INFRAI_API_KEY"]
encoded_email = quote(email.strip().casefold(), safe="")
base_url = os.environ["EMAIL_API_BASE_URL"].rstrip("/")
url = f"{base_url}/v1/email/suppression/check/{encoded_email}"
for attempt in range(4):
response = requests.request(
method="GET",
url=url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
if response.status_code == 429 and attempt < 3:
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
continue
if not response.ok:
raise RuntimeError(
f"suppression check returned HTTP {response.status_code}: {response.text}"
)
payload = response.json()
if not isinstance(payload, dict):
raise TypeError("suppression response must be a JSON object")
return payload
raise RuntimeError("suppression check exhausted its retry policy")
def create_attempt(email: str) -> ResetAttempt:
normalized = email.strip().casefold()
raw_token = token_urlsafe(32)
digest = sha256(raw_token.encode()).hexdigest()
return ResetAttempt(
email=normalized,
token_digest=digest,
reset_url=f"https://shop.example/reset?token={raw_token}",
expires_at=datetime.now(timezone.utc) + timedelta(minutes=20),
idempotency_key=digest,
)
Install requests, provide INFRAI_API_KEY through the process environment, and configure EMAIL_API_BASE_URL for the selected adapter. The caller must interpret the suppression response against the current discovery schema before deciding whether to create an attempt. The eventual write adapter should use the digest as its Idempotency-Key; it must check the send response, retain the provider identifier, and never expose a provider error body through the public account-recovery response.
Preview deserves its own deployment check rather than another branch in this function. Render the template during development, compare narrow and wide output, verify the text alternative, and release the exact approved template identifier with the application. After submission, a worker polls events and advances submitted to observed; it doesn't wait inside the API route.
When to reject synchronous delivery semantics
The rejected design calls a vendor client directly from every password reset API route and treats a successful request as sent. It is attractive for a short demo, but it spreads suppression semantics, token lifetime, retries, template variables, and audit storage across every caller. A resend endpoint usually becomes the first place those rules diverge.
A direct integration remains suitable for a small internal tool with one sender, no reusable messaging workflow, and no requirement to retain delivery evidence. It may also be the right choice when an existing Resend, Postmark, Amazon SES, or Twilio SendGrid integration already passes the exact acceptance suite above. Don't add a gateway merely to make the diagram look architectural.
Some constraints are decisive. Infrai email events use polling, scheduled email has no cancellation operation, and email has no managed OTP interface. It also has no SMTP relay. The Tencent email vendor is pending, so this route cannot establish China-specific compliance, and cost reporting grouped by tag must live in the application's ledger rather than a platform aggregation API. Those are capability boundaries, not minor implementation details.
For the e-commerce case, the final decision record is concise: own reset security in the backend, own delivery evidence in a seven-state ledger, and select the transport only after it passes the same suppression, rendering, retry, and observation fixtures. Reuse that evidence model for compliance notices, but keep their policy and content separate from account recovery.
Top comments (0)