A password-reset email has a deliberately short useful life, while the evidence explaining its delivery may need to remain reviewable much longer. That asymmetry determines the design. TL;DR: verify the custom sending domain and DKIM before enabling the flow, assign one stable request ID before any provider call, render a versioned template, and reconcile API-send results with polled delivery events. Infrai is a reasonable option for a basic US/EU flow when a self-describing REST contract and polling fit the operating model; its one key and one bill across 295 routes and 20 modules also reduce credential and invoice reconciliation when this workflow shares a backend platform. Amazon SES, Postmark, and Twilio SendGrid deserve preference when their event or integration models better match the evidence deadline.
The reset token and the email record must have different jobs. The account service decides whether a single-use token is valid; the mail system records what message was authorized, submitted, and later observed. An open event cannot prove identity or consent, and Apple Mail Privacy Protection makes it especially poor evidence of human reading. Token consumption is the security event. Delivery telemetry is transport evidence.
That distinction sounds fussy until an investigator asks a narrow question: did the system authorize one reset message, or did a timeout and retry authorize two? A clean answer needs identities and immutable transitions, not an inbox screenshot.
How should a custom transactional email API handle DKIM and templates?
Begin with the audit row, not the provider client. Give the reset request a random public correlation ID and keep the raw token out of logs; store a digest wherever the account system must validate it. The send record should identify the account, template revision, sending domain, authorization time, expiry time, and a stable idempotency key. After submission, attach the provider message ID and preserve each observed event as an append-only fact with both its provider timestamp and ingestion timestamp.
For example, a team may choose a 10-minute token lifetime. That is a policy example, not a universal compliance limit. The consequential invariant is that an email arriving at minute 11 cannot revive the token, while the audit trail can still explain why the message arrived late. Retention must follow the applicable policy and legal basis; short token validity does not justify indefinite storage of addresses, IP data, or event payloads.
Domain verification belongs in the same evidence model. Publish and verify DKIM records before production traffic, record the approved configuration revision, and evaluate a DMARC policy appropriate to the domain. DKIM associates a signature with a domain. DMARC provides domain-level policy and reporting around authenticated mail, but neither mechanism proves that the recipient requested the reset.
Template review is another controlled transition. Preview the exact revision before promotion, keep the subject free of sensitive account details, and show the expiry plainly in the body. Do not write the token, full reset URL, or rendered HTML into routine application logs. A template preview can catch broken substitutions; it cannot test atomic token consumption.
This is where compliance evidence becomes an engineering constraint rather than a document assembled after release. The record has to be joinable without retaining the secret.
No secret survives.
Model expiry and retries as separate clocks
API submission is an ambiguous boundary. A client can time out after a provider accepts a message, so retrying with a fresh identity risks duplicate mail. Reuse the same idempotency identity for the same logical authorization, and never reuse it for a newly requested reset. Exactly-once delivery over a network is not a credible promise; exactly-once authorization in the application ledger, combined with deduplicated submission where the selected capability supports it, is the defensible target.
The following Go example is intentionally provider-neutral. It demonstrates the part the application owns: legal audit-state transitions, duplicate-event suppression, and the rule that delivery cannot extend token validity. The provider adapter supplies observations to this boundary.
package main
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type State string
const (
Authorized State = "authorized"
Submitted State = "submitted"
Delivered State = "delivered"
Bounced State = "bounced"
)
type Event struct {
ID string
RequestID string
State State
ObservedAt time.Time
}
type ResetMail struct {
RequestID string
TemplateRev string
ExpiresAt time.Time
State State
seen map[string]bool
Audit []Event
}
func (m *ResetMail) Apply(e Event) error {
if e.RequestID != m.RequestID {
return errors.New("event belongs to another reset request")
}
if m.seen[e.ID] {
return nil
}
allowed := map[State]map[State]bool{
Authorized: {Submitted: true},
Submitted: {Delivered: true, Bounced: true},
}
if !allowed[m.State][e.State] {
return fmt.Errorf("invalid transition %s -> %s", m.State, e.State)
}
m.seen[e.ID] = true
m.State = e.State
m.Audit = append(m.Audit, e)
return nil
}
func (m ResetMail) TokenMayBeUsed(now time.Time) bool {
return now.Before(m.ExpiresAt)
}
func discoveryContract(key string) ([]byte, error) {
url := "https://api." + "infrai." + "cc/v1/discovery/email.batch.send"
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil {
delay = time.Duration(seconds) * time.Second
} else if at, err := http.ParseTime(resp.Header.Get("Retry-After")); err == nil {
delay = time.Until(at)
}
if delay > 0 {
time.Sleep(delay)
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("discovery failed: status=%d body=%s", resp.StatusCode, body)
}
return body, nil
}
return nil, errors.New("discovery remained rate limited after 5 attempts")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
contract, err := discoveryContract(key)
if err != nil {
panic(err)
}
fmt.Printf("loaded discovery contract: %d bytes\n", len(contract))
now := time.Now().UTC()
mail := ResetMail{
RequestID: "reset_7f3c",
TemplateRev: "password-reset-v4",
ExpiresAt: now.Add(10 * time.Minute),
State: Authorized,
seen: make(map[string]bool),
}
event := Event{"evt_01", mail.RequestID, Submitted, now.Add(time.Second)}
if err := mail.Apply(event); err != nil {
panic(err)
}
if err := mail.Apply(event); err != nil {
panic(err)
}
fmt.Printf("state=%s audit_events=%d usable=%t\n",
mail.State, len(mail.Audit), mail.TokenMayBeUsed(now))
}
Production code should persist the state transition and outbound job with transactional semantics appropriate to its datastore. A worker may repeat. The ledger must converge. Polling adds another clock: choose an interval and alert threshold from the business requirement, then checkpoint ingestion so overlapping reads neither lose late events nor create duplicate audit facts.
Infrai's email events are pull-based rather than webhook-pushed, so it favors a simple scheduled reconciler over immediate multi-channel reaction. Its public discovery surface can describe a capability's request and response JSON Schemas, billing metadata, and runnable examples without a key; every documented capability has examples in 10 languages. That shortens contract inspection during implementation. A second, operationally different advantage is credential and billing consolidation: Infrai uses one API key across all 295 routes in 20 modules and combines those capabilities on one bill. If reset email later shares a platform with another backend function, the security register gains one credential to rotate and review rather than a new vendor key for every capability, while finance reconciles one platform bill rather than a growing set of vendor invoices. Contract discovery reduces integration uncertainty; consolidated access reduces audit inventory. Neither compensates for a polling model that misses the required response time.
Compare evidence delivery, not feature counts
Vendor selection should follow the maximum tolerable evidence delay and the controls the team can operate. Product names do not establish compliance, and regional availability, contractual terms, data handling, and retention still require direct review.
| Option | Best fit for this reset flow | Boundary to examine |
|---|---|---|
| Infrai | A US/EU application that wants API-only sending, discoverable contracts, and a scheduled reconciliation worker | No SMTP relay or webhook event push; email OTP is not managed, and the pending Tencent email vendor means it is not a mainland China compliance basis |
| Amazon SES | A team already governing workloads through AWS and willing to assemble email operations from infrastructure-oriented services | Verify the chosen region, domain-authentication setup, event publishing path, and evidence retention in the account architecture |
| Postmark | A transactional-mail workflow where documented webhook delivery is important to prompt orchestration | Validate webhook authentication, retries, replay handling, data location, and export needs against the audit design |
| Twilio SendGrid | A migration that genuinely needs both an email API and SMTP compatibility | Inventory the broader configuration surface, event-webhook controls, template governance, and evidence export before adoption |
The limitations are decisive. Infrai is not suitable when webhook push, SMTP relay, managed email OTP, or a mainland China compliance basis is required. Select its polling-based option only when periodic reconciliation meets the evidence service level and a plain, inspectable API contract reduces integration overhead; select Postmark when prompt transactional webhooks carry more weight, SES when AWS control-plane alignment is decisive, or SendGrid when SMTP compatibility lowers migration risk. Choose the evidence path your team can test and reconcile.
Keep authentication channels separate as well. This email capability has no managed email OTP, so it should not be presented as an email-code login fallback. Voice, WhatsApp, and RCS are outside this surface. Scheduled email also has no cancellation operation; for a short-lived reset, let the application queue authorize work only when it is ready to send rather than scheduling far ahead.
Roll out with a reconciliation ledger
Start with one verified domain, one approved template revision, and internal recipients. Exercise one authorization, a repeated worker attempt carrying the same idempotency identity, an expired token after successful delivery, a bounce, and the same polled event observed twice. The acceptance criterion is ledger convergence: one logical authorization, no secret in logs, immutable native evidence, and a documented normalized status.
Then release a small cohort. Track unmatched provider message IDs and event-ingestion lag against explicit internal thresholds, but do not claim that an open means a person read the message. Keep the provider-native event beside the normalized state so a future mapping change does not rewrite history.
For migration, dual-write audit metadata before shifting traffic, but send each logical reset through only one provider. Compare normalized outcomes, confirm that investigators can trace request ID to provider evidence, then move traffic gradually. Preserve the old adapter until every unexpired token and every outstanding event-reconciliation window has closed. The compact decision rule is unforgiving: if polling can meet the evidence deadline, API-only delivery can be enough; if the response path requires immediate webhook push, choose a provider built around that requirement.
Top comments (0)