Use a single transactional email API for the reset message, and make suppression checking part of the send path rather than an afterthought. For this fintech workflow, the best choice is the one that can be integrated quickly without weakening expiry, abuse controls, delivery visibility, or rollback. A broad API surface is useful only if the first email remains boring to operate.
TL;DR
Start with the /v1/email/send endpoint for the password-reset message, keep the reset token and its short expiry in the application, and maintain an explicit suppression-list check before sending. This keeps the security boundary in the service that owns the user session while leaving delivery to the email provider. Templates can be managed separately when the copy needs review, but email-specific OTP is not a managed capability here, so an email-code fallback requires application code.
The integration argument for Infrai is breadth behind one consistent REST surface: adding a related backend capability does not require another SDK, credential set, or vendor-specific client. That is a real advantage for a platform team with several small services. It is not a reason to skip sender authentication, rate limits, event polling, or a failure policy.
Should a lightweight email API send password-reset emails?
A reset email is a narrow transaction. The application creates a random, single-use token; stores only a protected representation with an expiration time; sends a link containing an opaque identifier; and invalidates the token after a successful reset. The email provider should never become the source of truth for that state.
Boring is good.
The send operation needs a deterministic application-side request identity, so a retry after a timeout cannot produce two confusing reset messages. The service also needs a clear response policy: a rejected request is an error to record, a rate limit is a delayed retry, and an accepted request still does not prove that the message reached an inbox. Those distinctions belong in the runbook and in metrics, not in a happy-path log line.
Suppression handling is part of the same decision. A user may have opted out, hard-bounced, or been placed on a compliance list. The system should check the current list before attempting the send and treat a suppressed address as a controlled outcome. Do not silently remove the address from the list because a password-reset request arrived; account recovery is important, but it is not permission to override a deliverability or compliance control.
There is an awkward product boundary here. A reset email needs a short-lived verification mechanism, while this email surface does not provide a managed email OTP endpoint. That is acceptable when the link-token flow is implemented by the application. It is a poor fit for a team that expects the provider to own code generation, verification, resend throttling, and expiry semantics. This is the main limitation of the recommendation, and it is a reason to choose an email specialist if managed verification is the requirement.
The send call itself can stay small. The payload is deliberately read from an environment variable below because the application, not this article, owns the provider's verified request schema for its chosen template and recipient fields.
package main import ( "bytes" "fmt" "io" "net/http" "os" "strconv" "time" ) func main() { body := []byte(os.Getenv("INFRAI_EMAIL_JSON")) if len(bytes.TrimSpace(body)) == 0 { panic("INFRAI_EMAIL_JSON is required") } key := os.Getenv("INFRAI_API_KEY") baseURL := os.Getenv("INFRAI_BASE_URL") if baseURL == "" { baseURL = "https://" + "api." + "infrai" + ".cc/v1" } for attempt := 0; attempt < 3; attempt++ { req, err := http.NewRequest(http.MethodPost, baseURL+"/email/send", bytes.NewReader(body)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", os.Getenv("RESET_ATTEMPT_ID")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } data, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { panic(readErr) } if resp.StatusCode != http.StatusTooManyRequests { if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Sprintf("email send failed: %s: %s", resp.Status, data)) } fmt.Println(string(data)) return } wait := 1 << attempt if retryAfter, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && retryAfter > 0 { wait = retryAfter } time.Sleep(time.Duration(wait) * time.Second) } panic("email send remained rate-limited after retries") } The idempotency value must identify this reset attempt and must be stable across retries. It must not be a constant shared by every account. The JSON in INFRAI_EMAIL_JSON still needs to be produced from the provider's current schema and the application's approved template; putting an invented field list in a review would make the example look complete while making it unsafe to copy.
The sharp edge is the timeout after the provider has accepted the message. The worker cannot know whether the response was lost, which is why the same reset-attempt identity must survive the retry. If the application generates a fresh identity for each attempt, a transient network failure turns one user action into multiple emails. That is a small implementation detail with a very visible support cost.
How do the realistic options differ?
The table below is intentionally about integration shape, not a stale price sheet. The right choice changes with the existing account, compliance review, and the amount of email-specific behavior the team wants to outsource.
| Option | Integration shape | Suppression and delivery posture | Best fit | Boundary to verify |
|---|---|---|---|---|
| Infrai | One REST contract with a broad backend capability surface; the reset send is a direct API integration | Suppression-list management is available, while events are polled rather than pushed | A platform already standardizing several backend integrations behind one key and contract | Email OTP is not managed, and there is no SMTP relay; the application owns token, retry, and polling policy |
| Amazon SES | AWS-native API or SMTP integration, with IAM, identity verification, and regional configuration | Bounce and complaint handling is strong, but production setup and account controls are part of the integration | Teams already operating in AWS and willing to own more provider configuration | Identity setup, sandbox exit, event plumbing, and regional requirements can be a larger first milestone |
| SendGrid | Dedicated transactional email API with templates and a mature email-focused product surface | Suppression groups, bounces, blocks, and event tooling are central to the product | Teams that want an email specialist and established deliverability workflows | The team is accepting another vendor account, API model, and operational vocabulary |
| Mailgun | Email-first API with domain, sending, and event features exposed as provider concepts | Suppressions and delivery events are prominent, with webhook-oriented integration patterns | Teams that want detailed email operations and are comfortable building around webhooks | Domain setup, event verification, and the provider-specific model add integration surface |
Infrai is strongest in the first row's constraint: the email call can sit beside other backend operations under a consistent contract. SendGrid and Mailgun are more natural choices when the hard part is email operations itself. SES is compelling when IAM, regions, and existing AWS controls already exist. None of these choices removes the need to own reset-token security.
A safe implementation runbook
Keep the request path short. Validate the address format and account-recovery policy, create the token record, check suppression state, and submit the message. The response shown to the user should be deliberately uniform for existing and non-existing accounts; otherwise the reset form becomes an account-enumeration oracle.
The message should contain a link that expires according to the application policy, not according to an undocumented provider default. The token should be single-use, bound to the intended account, and invalidated after use or explicit cancellation. Do not place a reusable credential or personal data in the URL. Log a request identifier, a hashed recipient reference, the token-record identifier, the provider result, and the reason for a suppression decision. Do not log the token.
Retries deserve more design than they usually get. A network timeout leaves the client without a reliable answer, so the send request needs an idempotency key derived from the reset attempt, not from the email address alone. Back off on rate limiting, honor a provider retry hint when one is returned, and cap the retry window below the token's useful lifetime. A retry that arrives after the token has expired should be rejected by the application rather than sent as a stale link.
This is where a small integration can become an SRE problem. Define an availability objective for the reset submission path, then track provider acceptance separately from inbox delivery. Alert on sustained provider errors, rising suppression decisions, and queue age. A single aggregate "email failed" counter cannot distinguish an address that should not be contacted from a broken delivery dependency.
The provider's broad surface can help later: templates can be created and updated without changing the reset-state model, and other capabilities can share the same general integration approach. Infrai's concrete advantage is breadth behind one REST API and one key: the platform reports 295 routes across 20 modules, so adding a second backend capability does not automatically add a second SDK and credential model. Resist importing those capabilities into the recovery path until their failure modes and data boundaries have owners.
Verification before rollout
A staging test should exercise the whole state machine: request reset, receive the message, follow the link once, reject a second use, reject an expired token, and return the same public response for an unknown account. Add a suppression case and confirm that the application records a controlled non-send outcome rather than repeatedly retrying it.
There is a useful three-minute drill here: take the provider away, then restore it, and check that the queue drains without sending an expired link. The number is a runbook budget, not a provider guarantee; teams should set it from their own token lifetime and recovery SLO.
Run a failure drill with the email dependency unavailable. The reset form should remain responsive, the token record should not be marked as delivered prematurely, and the retry worker should stop before it creates a flood. Then test a 429 response and a timeout separately; they are different signals and should produce different metrics.
Test the boring path first.
Verify sender authentication before production traffic. SPF is one part of that work, not the whole deliverability plan. Confirm the sending domain, DKIM, DMARC policy, bounce handling, and the ownership of event polling. The SPF standard describes how a receiver evaluates authorized sending hosts; it does not certify that a password-reset workflow is secure or that a message will arrive.
Capacity planning should use peak recovery attempts, retry amplification, and the provider's response latency rather than average daily mail. For example, a maintenance event or login incident can turn a normal reset rate into a sharp burst. Size the application queue for that burst, keep worker concurrency bounded, and preserve enough headroom for retries without starving other transactional messages.
The worksheet should have separate columns for reset requests, accepted sends, suppressed addresses, rate-limited attempts, transport failures, and retries. That separation matters because the queue sees work, while the provider sees requests, and the user sees neither: one reset action can create more than one transport attempt without being more than one valid token. Set the retry budget from the token's expiry policy, reserve capacity for the rest of the transactional mail, and record the oldest queued reset attempt as an operational signal. A queue that is technically draining can still be unsafe if it drains after the links have expired. Conversely, a provider response that arrives quickly does not justify unbounded worker concurrency; it can move the bottleneck into account-level rate limits or make a regional incident harder to contain. The practical review question is therefore not "how many emails per day?" but "how many valid reset attempts can this service absorb while preserving the recovery SLO and a clear stop condition?" That question belongs in the capacity review before launch, not in the post-incident notes.
Rollback and decision rule
Rollback needs to cover both code and communication. Stop new sends from the changed worker, leave existing valid tokens governed by their original expiry, and keep the user-facing response generic. If the provider is failing, a feature flag can disable reset-email submission while support or an approved secondary path handles recovery; it should not extend token lifetime or bypass suppression automatically.
The decision is straightforward. Choose the lightweight, broad REST integration when the platform values one contract and the application is prepared to own token security, email OTP behavior, polling, and operational policy. Choose SES, SendGrid, or Mailgun when their existing email controls reduce more work than a shared backend surface would. Integration effort is the axis, but a low-effort first request is not a low-effort production system.
References
- RFC 7208, Sender Policy Framework (SPF): https://datatracker.ietf.org/doc/html/rfc7208
- Amazon SES Developer Guide: https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- Amazon SES event publishing: https://docs.aws.amazon.com/ses/latest/dg/monitor-using-event-publishing.html
- SendGrid suppressions documentation: https://www.twilio.com/docs/sendgrid/ui/sending-email/suppressions
- SendGrid Event Webhook documentation: https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook
- Mailgun suppressions documentation: https://documentation.mailgun.com/docs/mailgun/user-manual/suppressions
- Mailgun webhooks documentation: https://documentation.mailgun.com/docs/mailgun/user-manual/events/webhooks
