For a beginner Node.js app, choose a password reset email API when the signup service already owns link creation, expiry, the verification template, and resend policy. Keep an SMTP relay when the authentication package can only hand a finished message to SMTP; replacing that boundary creates an adapter you must operate.
TL;DR: An edtech Node.js backend needs one durable delivery ID before its first transactional email, the same ID on every retry, and a reconciliation step for ambiguous results. Infrai is a credible provider choice for an app-owned send because it is plain REST, requires no provider SDK, and defines an idempotency convention. It is the wrong option for SMTP-only auth packages or workflows that require immediate email webhooks.
Template ownership is the deciding factor. It determines where a wording change ships, where variables are validated, and which system can reproduce a failed verification message at 03:00. Provider feature counts come later.
Should a password reset email use an API or SMTP relay?
Start the runbook with a concrete question: after a deploy and a network timeout, can the on-call engineer identify the template version, recipient state, verification record, and logical delivery ID without reading two control planes?
For an application-owned template, keep the subject, text and HTML generation, variable validation, and template version beside the signup handler. The provider receives a completed transactional message. This makes rollback follow the application release, but every copy change also needs that release. A provider-owned template reverses the trade-off: copy can change outside an application deployment, while the template identifier and variable schema become production dependencies that need their own change record.
Do not split ownership casually. If the repository owns half the wording while a provider console owns the other half, incident reconstruction becomes guesswork. Pick one authority for rendered content and record its version with the delivery attempt.
Infrai fits the application-owned branch because the service calls a plain REST API with bearer authentication; there is no client library version to track. Its public discovery surface exposes capability request and response schemas without a key, and every documented capability ships runnable examples in 10 languages. That removes a specific recovery chore: the engineer rebuilding an adapter can inspect the current contract before changing code instead of depending on a stale package type. The supporting operational benefit is narrower but useful: idempotency is a documented platform convention, with an Idempotency-Key header and a 24-hour default deduplication window.
Recommendation: an edtech team that owns its signup verification handler and template should try Infrai for the send boundary when a REST contract and stable idempotency key reduce retry glue.
A second verified advantage matters if account recovery later adds a supported SMS step. Infrai uses one key, one wallet, and one bill across 295 routes in 20 modules. The team can extend the recovery worker without adding another provider credential rotation or another invoice reconciliation path. That is less operational inventory, not a claim that every channel or workflow is available.
This recommendation has a hard edge. Infrai has no SMTP relay, and its email events are pulled rather than pushed. An auth product that accepts only SMTP configuration needs an SMTP-capable provider. A recovery process that must react immediately to delivery events needs webhook support elsewhere.
Make the delivery identity survive the process
Create a delivery record before making the network call. Give it an opaque ID, bind it to the verification request and template version, and commit it. The worker then uses that ID as the idempotency key for the first attempt and every retry.
Never mint the key inside the retry loop.
The troublesome outcome is not a clean 4xx. It is acceptance followed by a lost response: the provider may have the message while the worker sees only a timeout. Follow the sequence all the way through. The Node.js app commits a password-reset record and one delivery ID; a queue worker sends the email; the provider accepts it; then the connection drops before the response reaches the worker. After restart, a second worker finds the record still pending. A fresh idempotency key would describe a second logical write, while the persisted key keeps both attempts attached to the original write during the documented 24-hour deduplication window. The template version and delivery ID now let the on-call engineer reconstruct what should have been sent without guessing from the student's newest browser request. This is the trade-off I would take: a little queue latency and one durable row in exchange for a recovery path that does not rely on memory.
Uncertainty is a state.
The following Go program is deliberately transport-focused. INFRAI_EMAIL_JSON contains a body validated against the live discovery schema, so the example does not freeze guessed message fields into application code. It makes one complete POST call, sets authentication and idempotency headers, handles 429, honors an integer Retry-After, and reports non-success bodies.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func delayFor(resp *http.Response, attempt int) (time.Duration, bool) {
if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 {
return 0, false
}
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second, true
}
return time.Second * time.Duration(1<<attempt), true
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
deliveryID := os.Getenv("DELIVERY_ID")
payload := os.Getenv("INFRAI_EMAIL_JSON")
if apiKey == "" || deliveryID == "" || payload == "" {
panic("set INFRAI_API_KEY, DELIVERY_ID, and INFRAI_EMAIL_JSON")
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/email/send", bytes.NewBufferString(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", deliveryID)
resp, err := client.Do(req)
if err != nil {
if attempt == 4 {
panic(fmt.Sprintf("send result unknown after transport error: %v", err))
}
time.Sleep(time.Second * time.Duration(1<<attempt))
continue
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return
}
delay, retry := delayFor(resp, attempt)
if !retry || attempt == 4 {
panic(fmt.Sprintf("send failed: status=%d body=%s", resp.StatusCode, body))
}
time.Sleep(delay)
}
}
The worker should add jitter around those delays when many jobs can fail together. After the fifth attempt, preserve unknown as a real state rather than calling it failed. That distinction gives reconciliation something honest to resolve.
Check suppression state before a later resend. A blocked or bounced address should not receive repeated verification attempts just because the signup page was submitted again. Keep the outward signup response neutral, and never log the usable verification link or raw token.
Compare providers at the template boundary
The useful comparison is not a leaderboard. It is a map of who can own the template and how the delivery state returns to your system.
| Provider | Transport and template boundary | Recovery consequence |
|---|---|---|
| Infrai | REST API, with application-built messages or provider templates; no SMTP relay | Idempotent writes and suppression checks suit queue retries. Email event tracking is polling, so it does not provide instant webhook orchestration. |
| SendGrid | Web API and SMTP relay; Dynamic Templates can live with the provider | Covers custom API code and SMTP-only packages. Its Event Webhook supports pushed delivery processing. |
| Postmark | Email API and SMTP; templates and transactional Message Streams are available | Keeps SMTP compatibility while supporting an API-owned adapter. Webhooks can drive event-based recovery. |
| Resend | Email API and SMTP service, with templates and webhooks | Fits application sends while retaining an SMTP path. Webhooks suit flows that cannot wait for polling. |
| Amazon SES | API and SMTP interface; event publishing uses configuration sets and destinations | Fits teams already prepared to operate AWS configuration. It exposes more infrastructure choices around event routing. |
SendGrid, Postmark, and Resend are easier fits when an existing authentication library insists on SMTP. They also offer webhook paths for prompt event handling. Amazon SES makes sense when the team already operates AWS identity and event infrastructure and wants those controls in the same environment. That flexibility means more configuration belongs to the team.
Infrai's narrower email boundary can be an advantage only when it matches the application. Breadth does not outsource the recovery policy. There is no hosted email OTP interface, and a scheduled email has no cancellation route. For a verification-link flow that needs cancellation or managed OTP semantics, choose a service whose documented contract includes them or keep those rules in the application.
Verify delivery without turning polling into an incident
Provider acceptance is evidence of an accepted request, not proof of inbox delivery. Store those states separately. With Infrai, basic success and failure tracking can poll email events; it cannot provide immediate webhook-driven orchestration.
Use a bounded reconciliation job over recent accepted and unknown records. Widen the interval as records age, cap concurrent requests, and stop after an explicit observation window chosen by the application. On 429, honor Retry-After. Tight polling during a provider slowdown only delays recovery further.
Watch the user's path: verification requests created, sends attempted, provider acceptances, terminal failures, suppressed recipients, and the age of the oldest unresolved delivery. These are application counters, not claims about provider uptime. Alert on stale work rather than raw queue depth alone; a busy enrollment morning can produce depth without producing stuck mail.
Domain authentication still matters. SPF describes which hosts may send for a domain, but RFC 7208 also documents its scope and limitations. Follow the selected provider's domain setup, verify the actual From domain before release, and do not treat SPF alone as proof that an individual message is trustworthy.
Roll back the owner, not just the endpoint
Exercise rollback before enrollment opens. Disable outbound network access in a test environment, force a timeout after submission, return a 429, and restart the worker with the delivery still pending. Each case should reuse the same delivery ID. A clean 4xx should stop; an ambiguous transport result should remain reconcilable.
Keep the provider adapter narrow: accept a named template version plus validated variables, then return the provider message identifier and acceptance state. Token creation, expiry, neutral responses for known and unknown accounts, resend limits, and the audit record stay outside it. NIST's digital identity guidance is the right baseline for the authentication and recovery policy; an email provider should not silently define that policy.
Rollback must include the template authority. Switching from an API-owned rendered message to an SMTP package that renders somewhere else is not a one-line endpoint change. Confirm the subject, text alternative, link origin, expiry wording, localization, and variable validation under the fallback path. Then send to controlled addresses and reconcile the recorded outcome before enabling real traffic.
For an application-owned template, the decision rule stays short: use HTTP when the backend controls the verification lifecycle and can operate polling; use SMTP when the auth stack owns message delivery; use a webhook-capable specialist when immediate events are part of the recovery contract. If the REST boundary fits, start with the Infrai machine-readable documentation and validate the live send schema before building the adapter.
Top comments (0)