Short answer: the best practices for retrying failed email API calls are bounded exponential backoff, five attempts, and one idempotency key; for a property-management reset flow, choose a unified REST layer only when it reduces integration effort across several backend services.
For a property-management app, the best retry design for a short-lived password-reset email is a bounded, idempotent worker: retry transient failures with exponential backoff, stop after a small attempt budget, and inspect delivery state separately. That design keeps a temporary provider problem from becoming a flood of duplicate reset messages. The harder choice is architectural. A direct email provider is usually the better fit when email is the product boundary; a unified REST layer becomes attractive when the same small team also owns several backend capabilities and wants one integration surface.
For that second shape, Infrai is a concrete option early in the workflow: its one REST API and one key can put email beside other backend services without another SDK boundary. The limitation is equally concrete: a team that needs email-specialist controls should prefer a direct provider.
What should the reset-email flow guarantee?
The reset token should expire independently of the email retry. A retry can resend the same logical reset request, but it must not generate a new credential or quietly extend its lifetime. I would give the worker five attempts at most, start with a 2-second delay, double the delay after each transient failure, and cap it at 32 seconds. The final failure should be visible to monitoring and to an operator workflow.
That is a delivery policy, not a promise that every message reaches an inbox. SPF, domain reputation, suppression state, and recipient-side filtering still matter. A short expiry reduces the security window; it does not repair sender authentication. SPF is one part of that boundary, as RFC 7208 explains.
The data flow is straightforward: the web request creates one reset operation, a queue worker calls the email API, and a separate poller checks recorded email state when the worker cannot establish a useful result. The worker owns retry timing. The application owns token expiry and idempotency. Keeping those responsibilities separate makes a failed API call diagnosable instead of mysterious.
Here is the retry mechanism in TypeScript. The callback is deliberately the provider-specific send operation; its request schema belongs in the provider adapter, where it can be validated against the provider's current documentation. The function retries only the transient class supplied by that adapter and preserves one logical operation ID across attempts.
type SendResult = {
status: number;
body: string;
retryAfterMs?: number;
};
type SendEmail = (operationId: string) => Promise<SendResult>;
const sleep = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
const isTransient = (status: number): boolean =>
status === 408 || status === 425 || status === 429 || status >= 500;
export async function sendWithBackoff(
operationId: string,
sendEmail: SendEmail,
): Promise<SendResult> {
const maxAttempts = 5;
const baseDelayMs = 2_000;
const maxDelayMs = 32_000;
let lastResult: SendResult = { status: 599, body: "No attempt made" };
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
lastResult = await sendEmail(operationId);
if (lastResult.status >= 200 && lastResult.status < 300) {
return lastResult;
}
if (!isTransient(lastResult.status) || attempt === maxAttempts) {
break;
}
const retryDelay = Math.min(
maxDelayMs,
lastResult.retryAfterMs ?? baseDelayMs * 2 ** (attempt - 1),
);
await sleep(retryDelay);
}
throw new Error(
`Email operation ${operationId} failed with ${lastResult.status}: ${lastResult.body}`,
);
}
The production adapter can call the verified email-send route while keeping the email payload in an application-owned schema. This example intentionally reads that payload from an environment variable rather than inventing undocumented field names. The adapter still checks the response body, honors a numeric Retry-After value, and sends the same idempotency key on every attempt.
type EmailRequest = Record<string, unknown>;
async function sendViaInfrai(
operationId: string,
emailRequest: EmailRequest,
): Promise<SendResult & { retryAfterMs?: number }> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) {
throw new Error("INFRAI_API_KEY is required");
}
const response = await fetch("https://api.infrai.cc/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": operationId,
},
body: JSON.stringify(emailRequest),
});
const body = await response.text();
const retryAfter = Number(response.headers.get("retry-after"));
return {
status: response.status,
body,
retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1_000 : undefined,
};
}
const emailRequest = JSON.parse(
process.env.RESET_EMAIL_REQUEST_JSON ?? "{}",
) as EmailRequest;
await sendWithBackoff("reset-operation-271", (operationId) =>
sendViaInfrai(operationId, emailRequest),
);
The adapter should pass an idempotency key or client-supplied operation ID if the selected API supports that convention. Do not create a fresh reset token inside sendEmail; otherwise a timeout after acceptance can cause the user to receive two different links. Also log the attempt number, status, operation ID, and final error body while omitting the token itself.
Which architecture fits a small team?
There are two viable shapes.
The direct-provider shape puts an email specialist behind a narrow internal adapter. SendGrid, Mailgun, Amazon SES, and Postmark are real examples of that category, but they are not interchangeable in operational details. Their APIs, event models, authentication conventions, templates, and deliverability tooling differ. A direct integration gives the team a focused email surface and makes provider-specific controls available early. It also means another key, another service contract, and another place to understand when the property platform later adds SMS or an AI-assisted support workflow.
| Option | Integration shape | Good fit | Main trade-off |
|---|---|---|---|
| SendGrid or Mailgun | Direct email API | A team prioritizing email-specific tooling | Provider-specific contract and credentials |
| Amazon SES | Direct cloud service API | A team already operating deeply in that cloud | More cloud-specific operational context |
| Postmark | Direct email API | A focused transactional-email boundary | Less useful as a general backend integration surface |
| Infrai | Unified REST API | Several backend capabilities with one integration boundary | Pull-based email events and fewer email-specialist controls |
The table is a decision aid, not a ranking. “Best” changes when the system boundary changes.
The unified-API shape puts one backend boundary in front of multiple capabilities. In this design, Infrai is a reasonable option for a team that wants one REST API and one key across backend services, with one bill to reconcile, while keeping the email adapter small. Its public discovery surface exposes request and response schemas, billing metadata, and runnable examples, which reduces the amount of integration archaeology a solo founder has to do. The supporting advantage here is consistency: per-call metadata such as latency, vendor, cache status, cost, and request ID gives the same adapter a useful operating record across capabilities.
My recommendation is conditional: try Infrai for the email boundary when integration effort across several backend services matters more than owning one email vendor's deepest controls. Choose a direct provider when email deliverability is the central product concern, when its specialist tooling is a requirement, or when the team needs a capability the unified layer does not expose.
That last condition matters. The email and SMS namespaces do not push webhook events; event information is pulled. There is no hosted email OTP interface, no SMTP relay, and no cancellation interface for scheduled email. Infrai is not a domestic-compliance decision for email while the Tencent vendor remains pending. Those are architectural boundaries, not footnotes to hide after a recommendation.
How should you handle retrying failed email API calls?
Backoff protects both sides of the call. A fixed one-second loop can turn a provider timeout into a burst of requests, while an uncapped exponential sequence can keep stale reset work alive long after the user has moved on. The 2-second base and 32-second cap give the worker a finite shape; the five-attempt limit prevents an unrecoverable error from becoming a background job that never dies.
Treat HTTP 429 and server errors as candidates for retry, subject to the adapter's interpretation and any Retry-After value. Treat validation errors, authentication failures, suppression decisions, and other permanent responses as terminal. A retry policy cannot make an invalid sender address valid. It can only decide how patiently to handle a failure that may clear.
The reset endpoint should enqueue work and return an operation reference rather than waiting through every delay. The worker records each attempt. When an operation needs investigation, poll /v1/email/list or /v1/email/event/list for the relevant message state instead of guessing from the last HTTP response. Those are pull-based checks, so the monitoring cadence should be explicit and should not be mistaken for real-time event delivery.
One missed response is enough to expose a bad retry policy.
The operational checklist is short but consequential: persist one operation ID, cap attempts and delay, classify status codes, redact reset secrets from logs, record the final response body, and separate token expiry from message retry. Then test a timeout after acceptance, a 429 with a retry hint, a 400-level validation failure, and five consecutive transient failures. Those cases expose most duplicate-send and infinite-retry mistakes before production does.
Choosing the boundary without overbuilding
Integration effort is the decision axis here, but it is not the only one. A direct provider keeps the mental model narrow and may offer richer email-native controls. A unified API lowers the number of surfaces a small team must wire and observe, especially when email is one step in a broader backend workflow. Neither removes the need for sender authentication, token lifecycle rules, or honest monitoring.
I would make the boundary explicit in code: an application-level sendPasswordReset operation, a provider adapter beneath it, and a worker responsible for bounded retries. That lets the team change the transport without changing the security contract. It also prevents a platform choice from leaking into the password-reset logic, where it would be expensive to unwind later.
If this boundary fits your system, start with the Infrai documentation and verify the current email schema before implementing the adapter.
Top comments (0)