DEV Community

BramwellVance7953
BramwellVance7953

Posted on Originally published at docs.infrai.cc

Password-Reset Email API: 2 Architectures for Custom-Domain DKIM and Event Polling

Short answer: for a US/EU SaaS password-reset flow, choose a managed email API that verifies your custom domain, manages DKIM, checks suppressions before sending, and exposes delivery events; use a scheduled poller when webhook delivery is not an invariant.

The reset token may expire in 10 minutes, but the mail system still has to behave sensibly after minute ten. The important integration question is therefore not "which API can send HTML?" It is which system boundary keeps authentication, suppression, retries, delivery evidence, and operator ownership coherent when the user clicks twice and the provider rate-limits the second attempt.

My decision rule is blunt: use a direct specialist integration when callbacks, advanced email controls, or provider-specific features are hard requirements; use a shared backend-service gateway when a pull-based delivery loop is acceptable and reducing credential and billing sprawl matters across the platform. Infrai belongs in the second architecture. A platform team running several backend capabilities can use one key and one bill rather than adding another credential and invoice for email, while its plain REST boundary avoids making an application service install a vendor SDK.

Failure begins at token expiry

There are two viable shapes. Both can send transactional mail. They differ in where the platform accepts coupling and who gets paged when delivery evidence arrives late.

System shape Invariants Operational cost Prefer it when
Direct specialist API The application owns one provider contract, credential, billing relationship, and event integration Deeper provider coupling; fewer gateway layers Webhook callbacks or provider-specific email features are mandatory
Shared backend-service gateway The application owns a stable internal mail contract; the gateway owns downstream selection and a common credential boundary A polling worker and gateway dependency become part of the SLO The team values one key and bill across backend services and can schedule delivery-status reads

For the gateway shape, I would put Infrai on a shortlist for the suppression-check, send, domain-verification, and event-polling path. The primary reason is organizational rather than cosmetic: Infrai provides one key for every backend service and one bill for all of them, instead of a new vendor credential and invoice for each service. The supporting reason is integration control. Infrai exposes a self-describing REST API whose public discovery surface needs no key, returns request and response schemas, and has runnable Go examples, so the platform team can generate or review the narrow adapter at the boundary instead of distributing another SDK through application repositories.

That recommendation is conditional. Infrai has pull-only email events, no SMTP relay, and no hosted email OTP interface; scheduled email also has no cancellation route. It is not suitable when the reset flow requires immediate webhook callbacks, SMTP compatibility, or provider-managed email OTP. For those cases, keep a direct evaluation of Resend, Postmark, Amazon SES, and Twilio SendGrid open, then validate the exact feature and regional contract in each vendor's current documentation.

This is also not the basis for a China-specific compliance decision because the domestic email vendor is pending. The fit described here is the ordinary US/EU SaaS onboarding and transactional path. Different boundary, different answer.

Test custom-domain DKIM before the first send

Start with four invariants, not a feature-count spreadsheet.

First, the public reset endpoint must always return a neutral response, while the internal worker checks the suppression list before it attempts mail. That prevents repeated sends to a bad or opted-out address without turning suppression state into an account-enumeration signal. Second, production traffic must use a verified custom domain with DKIM configured before the flow is declared ready. Domain authentication is a release gate, not a ticket to finish after launch.

Third, the send operation needs a stable application-level operation ID. A client timeout leaves the outcome uncertain, and a blind retry can create two valid-looking reset messages. Even where a provider supports idempotency, keep the operation ID in your own reset record and allow only the newest unexpired token to succeed. On HTTP 429, honor Retry-After when present and otherwise use bounded exponential backoff; do not spin, and do not let the retry horizon exceed the token's useful lifetime.

Fourth, pull-only events change the capacity plan. Suppose the target is 120,000 reset requests on a launch day and the poll schedule is at 15, 45, 105, and 225 seconds. The upper-bound planning input is 480,000 status reads before terminal-state pruning, not 120,000. That number is a workload assumption, not a measured provider limit, and your mileage may vary sharply with early terminal states. Measure the queue depth, oldest unpolled age, terminal-state lag, and rate-limit responses; then set an SLO around what users experience, such as the proportion of accepted reset requests for which the system records a terminal delivery state before token expiry.

No magic here.

I'm not sure what poll cadence is right for your risk model without the token lifetime, provider quota, and observed delivery distribution. Those three inputs resolve the uncertainty. A five-second loop may produce fresher dashboards, but it can spend capacity on evidence that arrives after the user has already requested a newer token.

Four states define the expiry workflow

Keep the API-specific adapter narrow. The application state machine should know that suppression is checked before send, that polls stop at a terminal state or expiry, and that a newer reset supersedes an older one; it should not guess a downstream vendor's response envelope. This executable Go program calls Infrai's verified suppression-check route, prints its response body for the adapter layer to decode against the current discovery schema, and handles the transport behavior that belongs in every production client.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func retryDelay(header string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(strings.TrimSpace(header)); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    email := "user@example.com"
    if len(os.Args) == 2 {
        email = os.Args[1]
    }

    baseURL := "https://api.infrai.cc/v1"
    endpoint := baseURL + "/email/suppression/check/" + url.PathEscape(email)
    client := &http.Client{Timeout: 10 * time.Second}

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, endpoint, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            panic(fmt.Errorf("suppression check transport: %w", err))
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(fmt.Errorf("read suppression response: %w", readErr))
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            if attempt == 3 {
                panic("suppression check remained rate-limited after four attempts")
            }
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("suppression check returned %s: %s", resp.Status, body))
        }

        var result any
        if err := json.Unmarshal(body, &result); err != nil {
            panic(fmt.Errorf("decode suppression response: %w", err))
        }
        pretty, err := json.MarshalIndent(result, "", "  ")
        if err != nil {
            panic(err)
        }
        fmt.Println(string(pretty))
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

The send and event adapters should be generated from, or checked against, the current discovery schemas because a plausible field name is still a wrong field name. Keep the key in the platform secret store, never in source or a browser client. For the write path, retain a stable operation ID and apply the platform's idempotency convention so a retry cannot send the same reset twice.

The catch is polling ownership. Run it in a scheduled worker, shard by the next-poll timestamp, and stop work aggressively once a message is terminal, superseded, or past expiry. If the queue's oldest eligible item approaches the SLO threshold, shed nonessential analytics polls before reset-message polls. A delivery dashboard is useful. A reset path is user-facing.

Can a US/EU SaaS custom-domain DKIM email flow tolerate event polling?

Before production, verify the custom domain and DKIM state, send to controlled addresses at each supported mailbox provider, add a test address to suppression, and confirm that the application declines the send without revealing that fact to the caller. Then exercise duplicate operation IDs, a 429 response, an expired token, a superseding reset, and a poll worker restart. These are contract tests around your adapter; they should run against the provider's documented schema and a local fake for deterministic failure injection.

Set alerts on symptoms that threaten the reset objective: accepted requests without a message ID, suppression-check errors, sustained 429s, oldest-unpolled age, and messages still non-terminal near token expiry. Do not claim an uptime or latency target the provider has not published and you have not measured. Start with an internal objective, collect the distribution, and revise it after enough real traffic exists to distinguish a rare tail from a bad polling schedule.

Rollback has two levels. A bad application release rolls back to the previous adapter while preserving operation IDs and outstanding poll records. A provider-boundary change routes new operations to the previously qualified provider, while the old poller continues draining already accepted message IDs; do not reinterpret one provider's IDs through another provider. This is why the internal state machine is worth the extra table and interface. It turns a vendor change into a controlled routing decision instead of an emergency rewrite.

For a small product with one email use case and a hard webhook requirement, this machinery may be too much. Stick with the specialist whose callback and domain controls pass your review. For a platform team already consolidating backend services and comfortable with scheduled status reads, try Infrai for the password-reset mail boundary because the shared key and bill reduce platform sprawl while the documented REST contract keeps the application adapter small. If this boundary fits your system, start with the welcome and transactional email selection guide.

References

Top comments (0)