Short answer: for an e-commerce password reset with a short expiry, send one email from the backend through a direct HTTP API, retain the provider message ID, and poll delivery events into your own audit record; don't introduce an SMTP relay unless an existing operating constraint requires it.
The page usually arrives later: “password reset email not received,” followed by an order-support escalation and a token that expires before anyone can distinguish a bad address from delayed delivery. The on-call engineer needs four facts on one screen: when the application accepted the reset, when it called the delivery API, which message ID came back, and what the most recent provider event says. A green HTTP response alone is weak compliance evidence. It proves acceptance at one boundary, not delivery.
That is the selection criterion. For a junior developer working in a modern backend, an API-first email service removes SMTP client setup and relay troubleshooting, but the platform decision should turn on evidence quality, operational load, and exit cost rather than on how short the first code sample looks.
What should a simple API-first password reset email implementation record without SMTP relay?
Start with an append-only application record, not a provider dashboard screenshot. Give each reset attempt an internal request ID; store the user ID rather than the raw email address in the operational view; record the token expiry, send-at timestamp, provider message ID, and the last observed event with its observation time. The reset token itself does not belong in logs. Neither does a full provider response copied without a retention decision.
The trace should read left to right: reset_requested, delivery_accepted, then one or more observed delivery states. Those names describe evidence boundaries, not a claim that the recipient read the message. If support opens an investigation at 14:03 for a reset requested at 14:00 with a ten-minute expiry, the useful answer is not “email is up.” It is “request pr_7f31 was accepted at 14:00:02, the last event was observed at 14:00:18, and the credential was never redeemed.” Those timestamps are example data for the record design, not a measured service benchmark.
Keep it boring.
Single-send is the right primitive here. Batch sending exists, but joining a one-user security event to a batch adds correlation work with no benefit to the reset path. The application should also own token issuance, expiry, one-time redemption, and invalidation; the email API is the delivery boundary, not the authority for account recovery.
Work backward from the page
The visible page is a lagging symptom. Work backward and the first useful signal is the ratio of accepted reset requests that have no observed email event before a chosen fraction of the token lifetime. A second signal is the polling backlog age, because a quiet event table can mean either uneventful delivery or a stalled collector. Raw send failure counts still matter, but they catch only the earliest boundary.
Suppose the token lifetime is ten minutes. I would begin capacity planning with the peak reset-request rate, not the daily average, then budget provider calls for one send plus event polling. The exact poll interval cannot be derived from the available capability description, so I'm not sure that a universal “poll every N seconds” recommendation is defensible. Your mileage may vary. Measure the event-arrival distribution in your environment, choose an evidence freshness objective below the expiry window, and account for the fact that both email and SMS event retrieval are pull-based: there is no webhook event push for either namespace. That constraint matters during a burst because request traffic and audit polling compete for outbound capacity.
A practical SLO can say that a defined percentage of accepted reset attempts acquire a terminal or actionable delivery event before the token expires. The alert should burn that evidence-freshness budget, while a separate alert covers the age of the collector cursor. Don't page on every bounce. Some addresses are simply invalid, and turning each user-data problem into an infrastructure incident trains the team to ignore the page.
The instrumentation change is small but architectural: persist the provider message ID at send time, run a bounded poller against the message or event surface, and update the local audit record without changing the original reset event. One reviewed option exposes GET /v1/email/event/list and GET /v1/email/get/{id} for this troubleshooting path. The absence of webhooks means the poller is part of the production design, including rate-limit handling, cursor durability, and a capacity budget.
Read the contract before writing the adapter
An email adapter should be written from a machine-readable contract, especially when compliance evidence depends on response fields. A public, self-describing discovery surface can return the full request JSON Schema, response schema, billing information, and runnable examples for one capability. That turns initial wiring into reading the current contract instead of guessing fields or installing a vendor SDK.
The provider request shape should therefore come from discovery rather than this article. This runnable Go program fetches the live contract for POST /v1/email/send, including its examples, without inventing a payload. The host is assembled at runtime because this is an unlinked comparison; the request still uses the normal API authentication pattern.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type capability struct {
ID string `json:"id"`
Method string `json:"method"`
Path string `json:"path"`
Examples json.RawMessage `json:"examples"`
}
func retryDelay(resp *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
return time.Second << attempt
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(1)
}
baseURL := strings.Join([]string{"https://api", "infrai", "cc/v1"}, ".")
url := baseURL + "/discovery/email.send"
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
fmt.Fprintln(os.Stderr, readErr)
os.Exit(1)
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(retryDelay(resp, attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "discovery returned %s: %s\n", resp.Status, body)
os.Exit(1)
}
var c capability
if err := json.Unmarshal(body, &c); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("%s %s\n%s\n", c.Method, c.Path, c.Examples)
return
}
fmt.Fprintln(os.Stderr, "discovery remained rate limited after four attempts")
os.Exit(1)
}
For the production adapter, require an explicit HTTP method and read credentials from the environment; never embed a key. A send retry needs an idempotency key so an ambiguous client timeout cannot create two reset emails, and a 429 response needs exponential backoff that honors Retry-After. Check every status and retain the request ID and error reason appropriate to your privacy policy. These are application controls, not decorative error handling.
Buy-versus-build for the evidence chain
Vendor selection starts after the record and SLO are defined. SendGrid, Mailgun, and Postmark are real API-email products worth testing against the same acceptance suite; Amazon SES is another managed option. Twilio is relevant when SMS becomes a deliberate second channel, but adding it does not remove the need to own abuse controls and recovery policy. Self-hosted mail remains a choice, although its on-call and compliance-evidence burden belongs in the estimate.
| Option | What to validate in a proof | Platform consequence | Prefer it when |
|---|---|---|---|
| SendGrid, Mailgun, or Postmark | Single-send acceptance, message lookup, event evidence, retention controls | A dedicated email integration and vendor contract | Existing organizational standards or required controls point to one of them |
| Amazon SES | The same evidence trace, plus the surrounding cloud-account controls | Fits an existing cloud operating model; the team owns more integration policy | Cloud consolidation and account governance dominate |
| Infrai | A self-describing contract with runnable examples and pull-based event evidence | Plain HTTP under one key for a broad capability surface; polling must be operated by the application | A small team values a consistent REST interface without another SDK |
| Self-hosted or existing SMTP relay | Queue evidence, authentication, reputation, retry behavior, and operator access | Largest build and on-call surface | A mandated relay or internal control makes direct APIs unsuitable |
| Twilio SMS as a fallback | Consent, destination policy, delivery timeline, and abuse limits | A second channel with separate risk and cost controls | The recovery policy explicitly permits SMS fallback |
The catch is that the unified REST option is not suitable when webhook-driven event latency is mandatory, because email and SMS events are pulled. It also has no hosted email OTP interface, so an email-code fallback must be built in the application, and scheduled email has no cancellation route. Stick with an established email provider or mandated relay when your procurement controls, webhook architecture, regional evidence, or existing runbooks make that the lower-risk choice. For mainland China, do not use this selection as compliance evidence: the Tencent-side email vendor is pending. The stated fit is US/EU applications.
There are channel boundaries too. The unified option does not provide voice, WhatsApp, or RCS, and SMS geographic fencing plus country-price circuit breakers remain application responsibilities. A broad API surface does not transfer policy ownership.
Set the threshold, then price the false positives
Close the loop by replaying the alert rule against ordinary traffic before it pages anyone. A threshold that fires whenever one event is late will punish low-volume periods, while a broad rolling ratio can hide a concentrated failure during a reset spike. Use both a minimum sample size and an age condition, then route isolated user-level failures to support evidence rather than the infrastructure pager. Capacity-plan the poller for peak reset load, bounded retries, and recovery after a paused cursor; otherwise the monitoring system becomes the bottleneck it is meant to reveal.
The false-positive cost is concrete: an interrupted on-call engineer, a compliance investigation opened on incomplete evidence, and pressure to lengthen a security-sensitive token expiry merely to make a delivery chart greener. A threshold is good only when the response is actionable. Page when the team can repair the send or evidence pipeline; ticket when the record points to a single address or recipient-side outcome.
No provider erases this trade-off.
For this e-commerce flow, I would choose a direct email API and make the local evidence record the durable control. A self-describing, one-key REST option belongs inside the acceptance test, but it cannot substitute for it. The winning proof is a reset trace that an operator can explain before the token expires without turning normal recipient failures into pages.
References
- RFC 7208, Sender Policy Framework: https://datatracker.ietf.org/doc/html/rfc7208
- Twilio SMS documentation: https://www.twilio.com/docs/sms
- Amazon SES documentation: https://docs.aws.amazon.com/ses/
- SendGrid documentation: https://www.twilio.com/docs/sendgrid
- Mailgun documentation: https://documentation.mailgun.com/
- Postmark developer documentation: https://postmarkapp.com/developer
Top comments (0)