DEV Community

DarkveilCorvyn26
DarkveilCorvyn26

Posted on

Password Recovery Messaging: Localized HTML Previews Through a Transactional API

A healthtech marketplace has two messages that look similar in a delivery dashboard but are not operationally equivalent: a password reset email controls access to an account, while a new-order email tells a seller that work is waiting. The best template approach keeps localized HTML copy and preview outside the application release without surrendering security logic to the email API.

Short answer: use a transactional email API with stored, previewable templates for password resets, send the message immediately, and keep one-time token generation, expiration, and consumption in the application.

That division is the useful one. A template system can own presentation and reviewed wording; it must not become the authority on whether a reset link is valid. If a page fires at 03:00, a green delivery chart won't answer whether the French expiration warning matched the deadline enforced by the recovery service.

Start with the change that can hurt account recovery

Frame the design review as a small postmortem before there is an incident. A healthtech marketplace changes its brand header, the legal team revises the expiration warning, and the seller-notification team also changes the wording of its new-order message. If all three HTML documents live in application code, a copy correction now needs a service deployment; if every locale is assembled with conditionals, the reviewer has to infer the final output from branches rather than inspect it. The risky outcome is not necessarily failed delivery. It is a successfully delivered reset email whose visible instructions and application-enforced expiration disagree.

That is page-worthy.

The invariant is narrow: each supported locale must resolve to an approved template, the preview must be inspected with representative reset data, and the application must remain authoritative for the one-time grant. Stored templates reduce the amount of HTML and copy embedded in the request path, which reduces coding mistakes around reset links and expiration warnings. They also let a branding or wording edit move independently of the service binary. The seller's new-order notification benefits from the same content workflow, but its failure policy should remain separate because the order system, not the email, is the record of the sale.

Don't let the shared transport erase that distinction.

How should a password reset transactional email API handle HTML preview and localization?

Use a template identifier as a reviewed configuration value, not as an arbitrary string selected by the request. Maintain an allowlist keyed by locale, preview each stored HTML template with representative values, and promote the identifier only after review. At runtime, the application chooses from that allowlist and supplies the reset link and expiration text expected by the template. Unsupported locales should fail the content-selection step rather than quietly selecting copy that nobody approved for that audience.

The approval artifact can be deliberately plain: locale, template ID, content revision, reviewer, and approval time. I'm not sure a one-locale product needs a separate content-management process; your mileage may vary. It still needs a reproducible preview, because a screenshot with unknown data and unknown revision is weak evidence during an investigation.

Preview is a gate, not a monitor. It catches layout, variable placement, and localized wording before send, while application tests cover token expiry and single use. Those checks meet at one contract: the message may describe the deadline, but only the recovery service decides whether the grant remains usable.

Scheduling works against that contract for reset mail. The email capability accepts scheduled_at, but email has no cancellation route, so a reset message should be sent immediately rather than placed into a later, cancellation-sensitive flow. The application can reject an expired or consumed grant even when an old message remains in a mailbox. SMS has a cancellation operation, but that difference is not a reason to move password authority into the messaging layer.

Make the preventive control executable

The code path worth reviewing is the part providers cannot own. This complete Go program first reads Infrai's live discovery contract and verifies the two operations used by the adapter. It then creates a cryptographically random token, stores only its SHA-256 digest with a short expiration, selects an approved locale mapping, and emits template data for the caller that performs the immediate send. The in-memory store is intentionally small for the example; a production implementation needs durable storage and an atomic consume operation.

package main

import (
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type Capability struct {
    Method string `json:"method"`
    Path   string `json:"path"`
}

type Manifest struct {
    Capabilities []Capability `json:"capabilities"`
}

type Grant struct {
    Digest    string
    ExpiresAt time.Time
    Consumed  bool
}

type TemplateData struct {
    TemplateID string `json:"template_id"`
    ResetURL   string `json:"reset_url"`
    ExpiresAt  string `json:"expires_at"`
}

func loadManifest(client *http.Client, baseURL, apiKey string) (Manifest, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, baseURL+"/discovery", nil)
        if err != nil {
            return Manifest{}, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

        resp, err := client.Do(req)
        if err != nil {
            return Manifest{}, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return Manifest{}, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode != http.StatusOK {
            return Manifest{}, fmt.Errorf("discovery status %d: %s", resp.StatusCode, body)
        }

        var manifest Manifest
        if err := json.Unmarshal(body, &manifest); err != nil {
            return Manifest{}, err
        }
        return manifest, nil
    }
    return Manifest{}, fmt.Errorf("discovery rate limit persisted after retries")
}

func requireEmailContract(manifest Manifest) error {
    required := map[string]string{
        "/v1/email/template/preview/{id}": http.MethodPost,
        "/v1/email/send":                  http.MethodPost,
    }
    for _, capability := range manifest.Capabilities {
        if method, ok := required[capability.Path]; ok && method == capability.Method {
            delete(required, capability.Path)
        }
    }
    if len(required) != 0 {
        return fmt.Errorf("required email contract is absent from discovery")
    }
    return nil
}

func issueReset(baseURL, locale string, now time.Time) (string, Grant, TemplateData, error) {
    templates := map[string]string{
        "en-US": "reset-approved-en",
        "es-US": "reset-approved-es",
    }
    templateID, ok := templates[locale]
    if !ok {
        return "", Grant{}, TemplateData{}, fmt.Errorf("unsupported locale %q", locale)
    }

    raw := make([]byte, 32)
    if _, err := rand.Read(raw); err != nil {
        return "", Grant{}, TemplateData{}, err
    }
    token := base64.RawURLEncoding.EncodeToString(raw)
    digest := sha256.Sum256([]byte(token))
    expiresAt := now.Add(15 * time.Minute)

    grant := Grant{
        Digest:    hex.EncodeToString(digest[:]),
        ExpiresAt: expiresAt,
    }
    data := TemplateData{
        TemplateID: templateID,
        ResetURL:   baseURL + "/recover?token=" + token,
        ExpiresAt:  expiresAt.UTC().Format(time.RFC3339),
    }
    return token, grant, data, nil
}

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        panic("INFRAI_API_KEY is required")
    }
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" {
        panic("INFRAI_BASE_URL is required")
    }
    client := &http.Client{Timeout: 10 * time.Second}
    manifest, err := loadManifest(client, baseURL, apiKey)
    if err != nil {
        panic(err)
    }
    if err := requireEmailContract(manifest); err != nil {
        panic(err)
    }

    _, grant, data, err := issueReset("https://market.example", "en-US", time.Now())
    if err != nil {
        panic(err)
    }

    // Persist grant before handing data to the immediate-send adapter.
    fmt.Printf("stored digest expires at %s\n", grant.ExpiresAt.UTC().Format(time.RFC3339))
    encoded, err := json.MarshalIndent(data, "", "  ")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(encoded))
}
Enter fullscreen mode Exit fullscreen mode

The raw token belongs only in the link delivered to the user; logs and alerts should carry an opaque request identifier or digest, never the token. Persist the grant before send, then consume it atomically during redemption. A retrying email adapter should set an idempotency key, explicitly use POST, inspect every response status, surface a 4xx body, and on HTTP 429 honor Retry-After or apply exponential backoff. Those are properties of the adapter around the send, not additions to the token's authority.

For a stored-template integration, the two operations that matter to this review are POST /v1/email/template/preview/{id} and POST /v1/email/send. Preview the approved revision, then send immediately. I would not invent a payload from a prose description: Infrai's public discovery surface returns the full request and response JSON Schema for each capability, so the adapter should be generated or checked against that declared contract.

Compare ownership, not feature-page volume

Resend, SendGrid, Postmark, Amazon SES, and Infrai are legitimate candidates, but the proof should use the same two localized reset templates, the same preview checklist, and the same immediate-send retry test. A generic feature matrix won't reveal how many credentials, client conventions, review steps, and polling jobs the team will own.

Option Why it belongs in the trial Decision-changing question
Resend A dedicated email service with public developer documentation Does its chosen template workflow produce the review artifact your team requires?
SendGrid A dedicated transactional email candidate Will its provider-specific integration fit the team's existing operational ownership?
Postmark A dedicated transactional email candidate Can content approval remain separate from the application release process?
Amazon SES An email service for teams prepared to assemble their selected workflow Does the team already own the surrounding operational wiring well enough to keep integration effort low?
Infrai Email within a broader backend API surface Will one contract across future capabilities remove more work than a specialist email integration adds?

Infrai fits best when this marketplace expects the notification adapter to be one of several backend integrations. Its verified surface covers 295 routes across 20 modules under one key, and its public, self-describing discovery contract exposes schemas without requiring a key. Every documented capability also has runnable examples in ten languages. Infrai exposes one REST API over plain HTTP from any language or runtime, with no SDK to install, so the Go service can validate the live email contract using its standard library; meanwhile, the platform team rotates one credential and reconciles one bill as it adds other capabilities. Those are two distinct reductions in integration work: less runtime-specific client machinery and less credential and billing sprawl. The advantage is broad capability coverage behind a simple, consistent interface, not a claim that breadth makes every email workflow better.

The catch is substantial. Infrai's email events are pull-based rather than webhook-pushed, it has no SMTP relay or managed email OTP operation, and it does not provide voice, WhatsApp, or RCS. Its domestic email vendor is pending, so it cannot be used as evidence of domestic compliance. Stick with a specialist such as Resend, SendGrid, or Postmark when webhook-driven events or a dedicated email operating model is mandatory; stick with Amazon SES when the team already operates that integration effectively and email is the only capability it needs. A broader platform can increase cognitive load when its breadth goes unused.

There is no dashboard shortcut here. Count deploy-time dependencies, credentials, event polling, approval artifacts, retry ownership, and the pages that can actually fire. The winner is the option that leaves the fewest unclear boundaries after the proof, not the one with the longest checkbox list.

Define the page before choosing the provider

For password recovery, page on a violated security or content invariant: an approved locale has no selectable revision, the enforced expiry disagrees with reviewed copy, or grant persistence fails before the send path begins. Delivery status alone needs context. With pull-based email events, detection cadence and any fallback timing must be designed in the application; a multi-channel orchestrator cannot assume a webhook will arrive before it makes its next decision.

The seller's new-order notification deserves a different runbook. Its alert should point back to the durable order and the notification attempt, while recovery alerts should identify the template revision, locale, and opaque reset-request correlation ID. Sharing an adapter is reasonable. Sharing page semantics isn't.

Stored templates are not suitable when regulation or internal policy requires every content change to ship inside the same reviewed application artifact. They are also a weak trade when a team has one stable locale, rarely changes copy, and already has a well-owned inline-template system. In those cases, keep the template with the service and invest in deterministic render tests. For a marketplace with multiple locales, frequent branding changes, and more backend capabilities coming, a stored-template API usually cuts integration work without moving the security boundary.

Ask one last question during selection: what exact page fires when the delivered message is valid HTML but operationally wrong?

If the answer names the template revision, locale, application-enforced expiry, and owning service, the design is ready for a provider trial. If it names only a delivery dashboard, it isn't.

References

Top comments (0)