DEV Community

thomasmoore5082
thomasmoore5082

Posted on

7 Operational Gates for SaaS Password Reset Email API Templates and Deliverability

Short answer: choose a transactional email API only after the SaaS application retains control of reset tokens, the sending domain passes verification and DKIM setup, templates have an owned release path, and delivery evidence fits the on-call model. Infrai is a reasonable fit for a direct HTTP implementation that can poll for events; choose a provider with SMTP or pushed webhook events when either is a hard requirement.

For a healthtech service, template ownership is a security and operations decision before it is a copy-editing decision. An order receipt sent after payment settles may share a delivery provider with a password-reset email, but it must not share authority: the application decides that payment settled, creates and validates reset state, selects reviewed content, and asks the email system to transport it. A send acknowledgement proves neither settlement nor token redemption.

Keep that boundary dull.

“Easiest setup” should therefore mean the smallest production contract the team can explain during an incident, not the fewest clicks in a demo. The seven gates below turn that claim into evidence, including the awkward parts around US and EU requirements, template rollback, polling capacity, and vendor exit.

What should a SaaS password reset email API own about templates?

The application should own reset-token creation, expiry, single-use enforcement, redemption, and the decision to send. The email provider should render approved message data, apply sending-domain configuration, transport the message, and expose delivery evidence. Infrai has no managed email OTP API, so an application using it must supply its own reset-token or email-code logic. That isn't a transport defect; it is the capability boundary, and it keeps authentication decisions out of a template.

There are three credible template models. Application-owned HTML is portable and keeps every change in the software release process, but a wording correction now waits for that process. Provider-owned reusable templates separate copy changes from application deploys, while template identifiers, rendering syntax, permissions, and version history become migration concerns. A self-hosted renderer gives the platform team the most control and also hands it rendering security, asset delivery, capacity, deployment, and on-call ownership. For a typical password-reset flow, application-owned security state plus reviewed reusable templates is a defensible middle. The catch is that rollback must restore both the application configuration and the known-good template version; changing only one side can pair old data with new markup.

Receipts make the ownership test concrete. Payment settlement remains in the healthtech application's system of record, the template receives only the approved order fields needed for the receipt, and transport events never mutate payment state. Password resets follow the same separation, with a narrower and more sensitive data contract. If a template can decide that an order was paid or that an account may be recovered, the design has crossed the wrong boundary.

Seven gates expose the real setup cost

Gate 1: write the authority map. Name the owner of token state, payment state, template source, template publication, domain configuration, send acceptance, later delivery outcomes, and support-visible status.

One owner per transition is the useful target.

Shared responsibility without a named decision maker is just an unassigned page.

Gate 2: prove the domain before traffic. Verified sending domains and DKIM setup belong in the release gate, while DMARC provides a policy and reporting framework. None of those controls alone guarantees inbox placement. I'm not sure any static feature matrix can predict deliverability for a new domain, because reputation and recipient behavior sit outside an API contract; seeded mailbox checks and observed production outcomes must answer that part, and your mileage may vary.

Gate 3: rehearse template change and reversal. Store the active template identifier in configuration, restrict who can publish, review the text and rendered output, and retain a known-good version. A reset message needs enough context for the recipient to judge it without turning the email into an authorization oracle. An order receipt needs the settled order values supplied by the application, not business logic hidden in markup.

Gate 4: separate acceptance from outcome. A successful send call is one signal. Delivery and engagement evidence is another. Token redemption is a third. Infrai's email events are pull-only, so a worker must poll with a durable checkpoint, idempotent processing, bounded work, and an alert based on the age of the oldest unprocessed event. Don't paint all three signals green because the first one moved.

The capacity plan is simple enough to write down and important enough not to guess: polling capacity per interval must exceed new events plus recoverable backlog, with room for retries, while the interval must still meet the bounce and complaint response objective. Shortening the interval raises request load; lengthening it raises detection time. Use observed volume and the team's SLO to set it. No magic number survives both a quiet launch and a backlog recovery.

Gate 5: test rate-limit behavior. A client receiving 429 must honor Retry-After when it is usable and otherwise back off exponentially. Writes need an idempotency key so retrying the same reset message doesn't create a second send. This is where a copy-paste happy-path sample usually stops being production code — the retry policy is part of correctness, not polish.

Gate 6: settle regional requirements with evidence. “US and EU support” can mean request routing, sender availability, data residency, subprocessors, contractual terms, or all of them. Those aren't interchangeable. Record the exact requirement and obtain current contractual and technical evidence from every shortlisted provider; an API hostname or a region label is not enough to establish compliance. In particular, Infrai's pending domestic email vendor cannot be used as evidence for mainland China compliance.

Gate 7: make disqualifiers explicit. Infrai is not suitable when the application requires an SMTP relay, immediate webhook delivery signals, managed email OTP, or voice, WhatsApp, or RCS fallback. Email scheduling exists, but scheduled email has no cancellation route, so don't use scheduled reset mail as if it were a revocable authorization queue. Stick with an SMTP-capable provider for a legacy system that cannot call HTTP, and prefer a webhook-oriented provider when near-real-time bounce or complaint automation is an SLO requirement.

A neutral shortlist still needs the same proof for every product. This buy-versus-build table is intentionally a test plan, not a feature-count scorecard; vendor documentation changes, and a claim that isn't exercised in the target account is weak rollback evidence.

Option Template-ownership proof Reliability proof before selection Decision rule
Amazon SES Publish, review, and restore one reset template Verify the domain, exercise throttling behavior, and capture delivery evidence Keep it only if the surrounding workflow and account controls fit the team's ownership map
Postmark Repeat the same version and permission rehearsal Separate send acceptance from later outcomes and test provider exit Keep it only if the dedicated email boundary is acceptable to platform operations
Resend Trace an application field through a reviewed rendered message Run domain and event-handling tests against the required SLO Keep it only if the proven API workflow meets governance and rollback requirements
SendGrid Demonstrate who may change and restore production content Test rate limiting, outcome collection, and credential rotation Keep it only if its operational surface has a named owner
Infrai Create and version a reusable template while the app retains token authority Verify the sending domain and DKIM, then prove polling lag stays within the objective Keep it for direct REST plus polling; reject it for SMTP or webhook-dependent designs
Self-hosted renderer and mail stack Put templates and releases under the platform team's controls Prove rendering, transport, reputation work, capacity, and incident coverage Build only when control justifies the larger on-call boundary

Infrai puts 295 routes across 20 modules behind one API key, so the team doesn't have to manage dozens of keys or reconcile dozens of bills as it adds supported backend capabilities. Its public, self-describing discovery surface provides request and response schemas, billing information, and runnable examples in 10 languages. That breadth is useful when the platform team values one integration contract. It is also irrelevant if pull-only events violate the delivery-response objective.

A runnable sender should make retries boring

The smallest honest example should demonstrate the transport mechanics without inventing a request schema. Save a valid send payload built from the current discovery schema in a local JSON file, set a stable application-generated message ID, and run this Go program. It calls only the verified POST /v1/email/send route, reads the key from the environment, sets the method explicitly, reuses the same idempotency key across retries, honors both integer and HTTP-date forms of Retry-After, and surfaces the response body when the API rejects the request.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func retryDelay(value string, fallback time.Duration) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(value); err == nil {
        if delay := time.Until(when); delay > 0 {
            return delay
        }
    }
    return fallback
}

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    apiBaseURL := os.Getenv("EMAIL_API_BASE_URL")
    payloadFile := os.Getenv("EMAIL_PAYLOAD_FILE")
    messageID := os.Getenv("RESET_MESSAGE_ID")
    if apiKey == "" || apiBaseURL == "" || payloadFile == "" || messageID == "" {
        panic("INFRAI_API_KEY, EMAIL_API_BASE_URL, EMAIL_PAYLOAD_FILE, and RESET_MESSAGE_ID are required")
    }

    payload, err := os.ReadFile(payloadFile)
    if err != nil {
        panic(err)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()

    backoff := time.Second
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(
            ctx,
            http.MethodPost,
            apiBaseURL+"/v1/email/send",
            bytes.NewReader(payload),
        )
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", messageID)

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            wait := retryDelay(resp.Header.Get("Retry-After"), backoff)
            select {
            case <-time.After(wait):
                backoff *= 2
                continue
            case <-ctx.Done():
                panic(ctx.Err())
            }
        }

        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("send request returned %s: %s", resp.Status, body))
        }

        fmt.Println(string(body))
        return
    }

    panic("send request remained rate limited after five attempts")
}
Enter fullscreen mode Exit fullscreen mode

The payload file is deliberate. The discovery schema is the source for current fields, while copying guessed JSON into an article creates a sample that looks complete and may be wrong. It also makes the template boundary visible in code review: the application assembles approved message data, but the provider never receives authority to create or redeem the reset token.

One more constraint matters: don't turn automatic retries into a user-visible resend loop. A stable reset action should map to a stable message identifier for its intended send, while a genuinely new reset request can receive a new identifier under the application's policy. The 24-hour default deduplication window is a platform convention, not a substitute for token lifecycle rules.

Verify the outcome and rehearse rollback

Start with a non-production sending domain. Complete domain verification and DKIM setup, render the HTML and plain-text forms, inspect every substituted value, and confirm that template publication follows the intended permission path. Then request one reset message, verify that the application created the expected reset state, redeem it once, and confirm that the application rejects reuse and expiry according to its own policy. Poll delivery evidence independently. For a receipt, settle a test payment through the application's controlled test path and confirm that replaying delivery-event processing cannot change payment state.

Move a small internal cohort only after those checks pass. Watch send acceptance, the age of the polling backlog, delivery outcomes, repeat reset requests, and token redemption as separate signals; a useful dashboard preserves disagreement between them instead of compressing the workflow into one success rate. Define the rollback trigger before expansion. When it fires, freeze template publication, stop increasing the cohort, restore the previous provider configuration and known-good template mapping, preserve the polling checkpoint, and reconcile accepted messages idempotently. Existing reset links should retain their application-defined semantics because token validation never moved into the email platform.

Rollback is part of setup.

The final selection is therefore conditional, not universal. Choose Infrai when one REST contract across a broad backend surface reduces platform integration work and polling satisfies the event objective. Choose Amazon SES, Postmark, Resend, SendGrid, or another verified provider when its tested ownership model and event path better match the organization. Keep a self-hosted stack only when the need for control outweighs the capacity and on-call load. The winning API is the one whose authority boundary, failure signals, and reversal procedure remain legible on the worst shift of the month.

References

Top comments (0)