A password-reset email with a short expiry is useless if the sender cannot clear domain authentication. In a healthtech system, that failure also leaves a thin audit trail: the application may record an attempted reset while the email provider rejects the request before delivery.
TL;DR: Treat a 400-class "invalid from domain" response as a sending-identity problem first. Confirm that the account contains the exact domain used in From, inspect its verification state, fix or rotate mismatched DKIM records, wait for DNS propagation, and verify again. Only then debug the password-reset handler. My default implementation records the reset request, provider response, domain used, and message expiry without recording the token itself.
This order is faster because it tests the dependency that can invalidate every application-level retry. It also produces evidence an auditor can follow.
How should I troubleshoot a password reset email 400 bad request?
The useful distinction is where the rejection happens. A malformed reset link, an expired token, or a bad redirect can break the user journey after receipt. An unverified sending domain or stale DKIM record can stop the provider from accepting the email at all. Replaying the same application payload will not repair that trust relationship.
Start with the literal From address emitted in production. Extract its domain and compare it with the domains registered in the same provider account and environment. notify.example-health.com is not interchangeable with example-health.com, and a verified staging subdomain says nothing about the production subdomain. Config bloat makes this worse: one stale environment variable can quietly select the wrong identity.
Then inspect the provider's domain status and the published DKIM record. If the selector or value is stale or mismatched, rotate DKIM, publish the new DNS records, allow them to propagate, and re-run verification. Do not use repeated sends as a DNS probe. They add noise to the evidence trail and tell you less than a direct domain-status check.
The concrete diagnostic sequence is short:
- List the domains in the sending account.
- Match the exact production
Fromdomain. - Check its verification and DKIM state.
- Rotate DKIM only when the records are stale or mismatched.
- Publish the replacement records, wait for DNS propagation, and verify again.
- Send one new reset message with a fresh idempotency key.
Stop there if verification is not complete. Code changes are premature.
The smallest implementation I would ship
I keep token creation separate from email transport. The transport receives a ready-to-send message, while the application owns the short expiry, one-time use, idempotency, and evidence record. This avoids coupling security rules to any one provider SDK.
First, this runnable TypeScript check asks Infrai for the account's configured email domains. Set INFRAI_BASE_URL to the API v1 base supplied with the account; keeping it in configuration preserves this article's unlinked format. The response stays unknown on purpose because this diagnostic only needs the live domain records, not a made-up local interface. It makes at most four attempts, honors Retry-After, and surfaces the real response body on failure.
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!apiKey || !baseUrl) {
throw new Error("Set INFRAI_API_KEY and INFRAI_BASE_URL");
}
const sleep = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function listEmailDomains(): Promise<unknown> {
const url = `${baseUrl.replace(/\/$/, "")}/email/domain/list`;
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
const retryAfter = response.headers.get("retry-after");
const delayMs = retryAfter
? Number.parseFloat(retryAfter) * 1000
: 500 * 2 ** attempt;
await sleep(Number.isFinite(delayMs) ? delayMs : 500 * 2 ** attempt);
continue;
}
if (!response.ok) {
throw new Error(
`Domain list failed (${response.status}): ${await response.text()}`,
);
}
return response.json() as Promise<unknown>;
}
throw new Error("Domain list remained rate-limited after four attempts");
}
const domains = await listEmailDomains();
console.dir(domains, { depth: null });
Run that before changing the reset handler. The expected operational result is a record for the exact From domain whose status and DKIM details show that it is authenticated. If it is absent or unverified, correct the domain configuration and DNS first.
The application example below is also complete TypeScript. It intentionally uses a provider adapter because send-request fields differ across vendors, and inventing a universal email API would make the sample look runnable while hiding the risky part. The application code never logs the raw reset token.
import { createHash, randomBytes, randomUUID } from "node:crypto";
type ResetMessage = {
from: string;
to: string;
subject: string;
text: string;
idempotencyKey: string;
};
type SendResult = {
accepted: boolean;
providerMessageId?: string;
status: number;
error?: string;
};
type Evidence = {
eventId: string;
userId: string;
recipientHash: string;
fromDomain: string;
requestedAt: string;
expiresAt: string;
idempotencyKey: string;
accepted: boolean;
providerMessageId?: string;
providerStatus: number;
};
interface EmailProvider {
send(message: ResetMessage): Promise<SendResult>;
}
interface EvidenceStore {
append(record: Evidence): Promise<void>;
}
const hash = (value: string): string =>
createHash("sha256").update(value).digest("hex");
export async function requestPasswordReset(
input: { userId: string; email: string },
provider: EmailProvider,
evidence: EvidenceStore,
now = new Date(),
): Promise<{ token: string; expiresAt: string }> {
const from = "security@notify.example-health.com";
const ttlMs = 10 * 60 * 1000;
const expiresAt = new Date(now.getTime() + ttlMs).toISOString();
const token = randomBytes(32).toString("base64url");
const idempotencyKey = `password-reset:${input.userId}:${hash(token)}`;
const resetUrl = new URL("https://app.example-health.com/reset-password");
resetUrl.searchParams.set("token", token);
const result = await provider.send({
from,
to: input.email,
subject: "Reset your password",
text: `Use this link within 10 minutes: ${resetUrl.toString()}`,
idempotencyKey,
});
await evidence.append({
eventId: randomUUID(),
userId: input.userId,
recipientHash: hash(input.email.toLowerCase()),
fromDomain: from.split("@")[1],
requestedAt: now.toISOString(),
expiresAt,
idempotencyKey,
accepted: result.accepted,
providerMessageId: result.providerMessageId,
providerStatus: result.status,
});
if (!result.accepted) {
throw new Error(
`Email provider rejected password reset (${result.status}): ${result.error ?? "unknown error"}`,
);
}
return { token, expiresAt };
}
Ten minutes is an application decision in this example, not a universal compliance rule. The evidence store should be append-only under the system's retention and access policies. Persist a hash of the token wherever redemption is implemented, enforce single use, and invalidate it at expiresAt. Those controls sit outside email delivery, but they determine whether a successfully delivered reset is safe.
The adapter must map idempotencyKey to the provider's supported idempotency mechanism, send with an explicit method, surface non-success response bodies, and back off on HTTP 429. If Retry-After is present, honor it. A retry without idempotency can turn one reset request into several messages, which is confusing for users and ugly in an audit.
Compliance evidence changes the vendor choice
I benchmark this workflow by evidence completeness, not by a synthetic send loop. Time-to-first-call matters, but the first call is not the finish line. I want to answer four questions later: which authenticated domain sent the message, when was it accepted, which reset expiry applied, and which provider identifier ties the application event to delivery data?
No vendor name makes those answers automatic. Your application still needs an evidence record, token lifecycle controls, restricted log access, and a documented retention policy. CAN-SPAM is also not a substitute for health-data obligations; its business guide is useful email-law context, not proof that a healthtech password-reset workflow meets every applicable rule.
The product trade-offs are concrete:
| Option | Integration shape | Evidence and operational trade-off |
|---|---|---|
| Amazon SES | AWS service and identity model | Fits teams already centralizing access and audit operations in AWS; it brings AWS configuration surface with it. |
| Postmark | Transactional-email-focused API | A narrow product boundary is easy to reason about; cross-channel orchestration remains a separate system concern. |
| Resend | Developer-oriented email API | Its documentation makes a quick email integration approachable; the application must still retain its own reset evidence and token state. |
| Twilio SendGrid | Email API within a broader communications portfolio | Useful when an organization already operates SendGrid; account, sender, and event configuration still deserve explicit ownership. |
| Infrai | One REST surface with public capability discovery | Discovery exposes request and response schemas plus runnable examples, which reduces SDK glue. Email events are pull-based, so it is a weaker fit when real-time webhook-driven orchestration is mandatory. |
Infrai is a credible fit when a small team values a self-describing API and expects to add other backend capabilities under one key. Its discovery surface covers 295 capabilities, and documented capabilities include runnable examples in 10 languages. For this workflow, the second useful property is first-class idempotency across supported operations. Yet the boundaries matter: there is no SMTP relay, email has no managed OTP endpoint, and email delivery through the pending Tencent path cannot serve as evidence of China email compliance. Standard US/EU transactional email is the clearer target.
Amazon SES, Postmark, Resend, and Twilio SendGrid may each be the better choice when existing infrastructure, a focused transactional-email operating model, or an established communications account outweighs API consolidation. That is the fair decision. Switching vendors does not repair a DKIM record that your team failed to publish.
What I would change at scale
At higher volume, I would add a domain-readiness check to deployment rather than to every reset request. The release should fail if the configured From domain is absent or unverified. This catches drift before a patient or clinician needs a reset, and it keeps DNS diagnostics out of the latency-sensitive request path.
I would also separate three timestamps: reset requested, provider accepted, and user redeemed. They prove different things. Provider acceptance is not delivery, and delivery is not redemption.
Pull-only email events impose a real design cost. A scheduled poller needs a cursor, deduplication, backoff, and an explicit freshness objective. If the incident-response requirement depends on near-real-time callbacks, choose a provider with suitable webhook events instead of pretending a fast polling loop is equivalent. The same boundary affects multi-channel fallback: email has no managed OTP operation here, so an email-code fallback must be built and secured by the application. SMS anti-abuse geofencing and country-based spend circuit breakers also belong in application policy.
One more constraint is easy to miss. Scheduled email has no cancellation operation, although SMS does. I would not schedule password-reset mail anyway; mint the token when the user asks and send immediately. For other scheduled health communications, lack of cancellation can be disqualifying.
The decision rule
For a 400-class invalid-sender response, verify the exact sending domain and DKIM before touching application code. Re-test with one fresh, idempotent request after DNS verification succeeds. Preserve an evidence record that joins the reset request to provider acceptance without storing the raw token or recipient address in routine logs.
Choose the provider whose evidence path and operating model your team can actually own. A consolidated, self-describing REST surface is attractive for a lean team. An existing AWS estate, a focused transactional provider, or webhook-driven operations can reasonably point elsewhere.
The hard boundary is geographic: do not present a pending China email vendor path as China compliance evidence. Obtain the relevant legal and security review, plus working delivery infrastructure, before making that claim.
References
The primary documentation below is where I would verify sender-authentication setup, event behavior, and policy details before production rollout. Vendor features change; the application-level evidence requirements should not depend on marketing pages.
Sources
- Resend documentation: https://resend.com/docs/introduction
- Amazon SES verified identities: https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html
- Postmark DKIM guide: https://postmarkapp.com/support/article/1090-dkim
- Twilio SendGrid domain authentication: https://www.twilio.com/docs/sendgrid/ui/account-and-settings/how-to-set-up-domain-authentication
- FTC CAN-SPAM compliance guide: https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business
- RFC 6376, DomainKeys Identified Mail: https://www.rfc-editor.org/rfc/rfc6376
Top comments (0)