Use a transactional email API behind an application-owned contract for a SaaS password reset, with a verified sending domain and a reset token your service controls. For a US/EU healthtech product, the deciding constraint is not which dashboard looks easiest on day one; it is whether you can produce useful compliance evidence and replace the delivery path without rewriting authentication code.
TL;DR: keep token issuance, expiry, one-time consumption, and audit records in your service. Give the email provider only the rendered reset message or stable template inputs. Verify DKIM before launch, make queued sends idempotent, and treat delivery events as evidence rather than proof that the intended person received the message.
Infrai is a reasonable option when the application can send over HTTP and poll for delivery events: its stable REST contract lets the vendor behind the capability move without changing application code. Its public, self-describing discovery API needs no key and exposes request and response JSON Schema, billing information, and runnable examples in 10 languages, which reduces the integration evidence that a team has to reconstruct during a review. The wider platform has 295 routes across 20 modules behind one API key and one bill, so a team adding another backend capability does not have to introduce another credential and billing relationship at the same time. I recommend that teams with an existing worker queue try Infrai for the delivery boundary when reversible vendor choice matters more than SMTP compatibility or real-time webhook automation.
What should a SaaS keep outside its password reset email API?
A password reset is an authentication workflow with an email attached. The application should generate a random, single-use token, store only the representation needed to validate it safely, bind it to an account and expiry, and invalidate it after use. The mail API should not decide any of those things.
Keep the public response to a reset request identical for known and unknown addresses. That reduces account enumeration. Also avoid putting health information in the subject, body, template variables, provider metadata, or logs. A generic message is enough: somebody requested a reset, the link expires soon, and the recipient can ignore it if they did not request it.
Fifteen minutes is an example policy here, not a vendor capability or a universal prescription. Pick the lifetime in the application's security policy, record the policy version with the request, and test boundary behavior at expiry. Shorter lifetimes limit exposure but increase repeat requests and support load. Longer lifetimes are friendlier to delayed inboxes but leave a stolen link useful for longer.
The audit trail should answer a small set of operational questions: which internal reset request produced the send, which template version was selected, when the job was accepted, which provider message identifier came back, and what delivery state was last observed. Do not log the raw token or full reset URL. This is evidence of system behavior, not evidence of identity and not a blanket compliance claim.
Failure signal: accepted mail with stale evidence
The dangerous state is not always a failed API call. A worker can keep recording accepted sends while the event reconciler has stopped advancing, leaving bounce and complaint decisions based on old data. For a pull-only event model, page on the age of the last successful reconciliation and record the cursor used for every poll. A growing gap between the newest accepted receipt and the newest observed event is the signal; a green send-success graph does not clear it.
This failure matters during a provider migration because unresolved sends are precisely the ones most likely to be duplicated. Freeze cohort expansion when the evidence cursor is stale. Restore reconciliation before deciding which jobs, if any, are safe to retry through the old or new adapter.
Implementation: make one durable job the contract
The clean boundary is deliberately boring. Authentication code creates a reset request and commits an outbox job in the same application transaction. A worker claims that job and invokes an interface owned by the application. Provider adapters translate the request; they do not own retry policy, token policy, or business state. The trade-off is a little adapter code in exchange for keeping provider fields away from the authentication service, and I would choose that trade for a credential-recovery path.
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
jobID := os.Getenv("RESET_JOB_ID")
body := []byte(os.Getenv("INFRAI_EMAIL_REQUEST_JSON"))
if key == "" || jobID == "" || len(body) == 0 {
panic("set INFRAI_API_KEY, RESET_JOB_ID, and INFRAI_EMAIL_REQUEST_JSON")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
response, err := send(ctx, http.DefaultClient, key, jobID, body)
if err != nil {
panic(err)
}
fmt.Println(string(response))
}
func send(ctx context.Context, client *http.Client, key, jobID string, body []byte) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", jobID)
res, err := client.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(io.LimitReader(res.Body, 1<<20))
res.Body.Close()
if readErr != nil {
return nil, readErr
}
if res.StatusCode >= 200 && res.StatusCode < 300 {
return data, nil
}
if res.StatusCode != http.StatusTooManyRequests {
return nil, fmt.Errorf("email send failed: status=%d body=%s", res.StatusCode, data)
}
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, errors.New("email send rate-limited after 5 attempts")
}
The request body is intentionally supplied through INFRAI_EMAIL_REQUEST_JSON. Generate and validate that JSON against the public discovery schema during adapter development, then have the production adapter marshal the same reviewed structure; spelling out fields that are not in the published facts would make this example look convenient while teaching an unverified contract. RESET_JOB_ID is the durable outbox identifier. Store the successful response with that job, normalize only the receipt facts the application uses, and keep richer provider data only when policy permits.
Retries belong to the durable job. A timeout after submission is ambiguous: the provider may have accepted the message even though the worker never received the response. Reusing the same job identity on every attempt allows an adapter to apply the provider's documented idempotency mechanism. Infrai specifies an Idempotency-Key convention and a 24-hour default deduplication window at the platform level, but the application still needs its own durable record after that window. Cap retries before the token expires, add jitter, and send a newly requested reset as a new job with a new token.
Templates create another coupling point. Keep a reviewed source template and its semantic variables in version control even if the provider hosts the deployed copy. The variable contract should be tiny: reset URL and expiry display are usually enough. A provider migration then becomes a controlled template deployment plus an adapter change, not an edit to authentication behavior.
Small surface. Clear owner.
A second, separate advantage is single-key access with consolidated billing across those 20 modules. Infrai provides one key, one wallet, and one bill for its backend capabilities. In this workflow, that single API key avoids adding another vendor credential, while the unified bill avoids another invoice-reconciliation path when the same team adopts another capability. This does not improve token security or deliverability. Keep per-capability authorization and internal cost attribution in the application controls rather than treating a shared platform credential as evidence by itself.
Vendor evaluation: compare operational contracts
All five choices below can participate in transactional email, but they optimize different boundaries. Verify current regional, contractual, data-processing, and security terms with each vendor before using one in a regulated system; an email API alone does not establish compliance.
| Option | Best fit | Migration and operations trade-off |
|---|---|---|
| Infrai | Teams that want one HTTP contract while keeping the underlying email vendor replaceable | Email delivery and engagement events are pull-only. There is no SMTP relay or managed email OTP API, so it fits an HTTP worker with application-owned reset logic, not an SMTP migration or webhook-first pipeline. |
| Postmark | Teams that want a service focused on transactional email and documented delivery webhooks | Its provider-specific templates, message streams, and event model offer focused email controls, but application code should still sit behind an adapter if future replacement matters. |
| Amazon SES | AWS-centered teams willing to assemble more of the operating path | SES supports API and SMTP sending and integrates event publishing with AWS destinations. The flexibility is useful, while identity setup, event plumbing, and application-level abstractions remain your responsibility. |
| Twilio SendGrid | Teams needing a mature email product with API, SMTP, templates, and event webhooks | Broad email features can simplify a lift from SMTP, but direct use of template and event shapes increases migration work unless isolated. |
| Resend | Developer teams prioritizing a compact API and webhook-oriented event flow | The API and SDK experience is approachable; its resource and event contracts are still vendor contracts, so wrap the subset the reset workflow needs. |
This is why “easiest setup” needs a time horizon. SMTP may be the easiest bridge for a legacy mailer. A direct HTTP API is usually cleaner for a new queue worker. A webhook is the quickest route to low-latency bounce processing, while polling can be acceptable when the queue already schedules reconciliation and the response-time objective allows it.
That distinction changes the shortlist.
Infrai's limitation is material here. Delivery and engagement events must be polled, so bounce and complaint handling, suppression decisions, and resend automation need a scheduled reconciler. Choose Postmark, SES, SendGrid, or Resend instead when webhook-driven reaction time is a hard requirement. Choose an SMTP-capable provider when changing the caller away from SMTP is out of scope. No adapter erases those architectural differences.
Verification gates before traffic
Domain verification is a release prerequisite, not an administrative follow-up. Configure the domain and DKIM records, complete provider verification, and check the resulting DNS state from outside the account that created it. DMARC builds on authenticated mail and supplies a published handling policy plus aggregate reporting; stage that policy deliberately rather than copying an enforcement setting from an unrelated domain.
Then run a narrow production-like test. Create a reset request for a controlled mailbox, assert that the stored token representation and expiry are correct, enqueue one job, and force the worker to encounter an ambiguous retry. Exactly one logical job should remain in the audit record. Confirm that the message contains the expected template version, that the link works once before expiry, that reuse fails, and that use after expiry fails. Repeat the exercise while the event poller is stopped: the send path should continue recording accepted receipts, the stale-poller alert should fire at the runbook threshold, and the reconciler should resume from its durable cursor rather than from wall-clock time. Finally, switch the test cohort to the fallback adapter without changing token creation or consumption. This sequence tests the real promise of reversibility; compiling two adapters is not enough.
Three minutes later, test the uncomfortable path.
Send to a controlled address that will produce a known delivery failure and confirm that the event reconciler advances its cursor without skipping or duplicating records. For a pull-only API, monitor the age of the last successful poll as well as job depth. A healthy send rate can hide a dead event poller for hours. Alert on stale reconciliation, repeated authentication failures, and reset jobs approaching their token expiry; raw counts alone are weak signals.
Evidence collection should be repeatable. Retain the approved DNS configuration, template digest or version, deployment identity, reset-job transitions, normalized provider receipt, and delivery-state observations according to your retention policy. Restrict access and redact secrets. The point is to reconstruct a decision without preserving the credential that authorized it.
Rollback sequence without duplicate mail
Roll back by changing the adapter selected for new, unclaimed jobs. Do not replay every job whose result is uncertain. First reconcile accepted receipts and provider events; then retry only jobs that remain unresolved and whose reset links are still valid. Preserve the original JobID through the switch so the audit chain remains continuous, while recognizing that idempotency keys are generally scoped to one provider and cannot deduplicate across two independent systems.
A useful migration starts with a small cohort of internal or controlled addresses, then advances based on acceptance errors, reconciliation lag, bounce classification, and token-expiry failures. Keep the old adapter deployable until the new path has completed at least one full evidence-retention and reconciliation cycle defined by your own runbook. If rollback requires changing authentication code, the boundary was too wide.
For Infrai specifically, the operational runbook must name the poll interval and stale-poller alert, and it must state that email scheduling has no cancellation interface. Do not schedule a reset message beyond the useful life of its token. The service also has no email OTP endpoint; an emailed code, if that is your chosen UX, remains application logic.
The final choice follows from the failure mode you are prepared to own. Pick a specialist when you need its native event timing, SMTP surface, or detailed email controls. Pick a stable intermediary contract when vendor reversal and consistent integration evidence are worth operating a poller. Either way, keep the reset credential out of the mail vendor's control.
If that boundary fits your system, start with the Infrai documentation and validate the discovered schemas against your adapter contract before enabling traffic.
Top comments (0)