TL;DR: When comparing a Resend alternative, keep the transactional email template under application-team ownership and put a narrow API contract in front of delivery. For a European logistics application sending welcome messages and short-lived reset links from a custom domain, the useful question is not which dashboard has the nicest editor. It is whether changing the delivery system leaves token issuance, expiry, rendering, suppression policy, GDPR review evidence, and incident evidence intact.
That choice has a cost: owning the template means owning tests, deployment, accessibility, and review. I would still pay it for password resets because the message is part of the authentication path, and authentication behavior should not quietly change when the mail vendor does. The page that fires should say that reset delivery is failing, not merely that a vendor graph moved.
What should a Resend alternative transactional email API preserve?
Start with a bounded incident scenario. A dispatcher at a European logistics depot requests a reset while locked out of the routing console. The application creates a short-lived token, but the email provider is degraded. If the template, variable names, and sending call are tangled into that provider's SDK, a failover can turn into an emergency rewrite precisely when the expiry clock is running.
No invented outage statistics are needed to see the failure mode. The invariant is enough: a provider change must not change the reset token, its expiry, the recipient, the custom-domain sender, or the rendered security wording. Delivery is replaceable; authentication semantics are not.
Dashboards do not prove that invariant. A contract test does.
The boundary also makes GDPR work easier to reason about, although it does not make a system compliant by itself. Keep the message payload minimal, define retention and access rules outside the delivery adapter, document every processor involved, and get legal review for the actual regions and contracts in use. A European recipient is not evidence of data residency, and a custom domain is not evidence of compliance.
Step 1: Make template ownership explicit
Put the subject and body renderer in the application repository. Pass the provider only a finished message. The expiry remains configuration rather than a magic number, so the security team can change policy without editing vendor-hosted markup.
This complete program renders a plain-text reset message and refuses ambiguous inputs:
package main
import (
"errors"
"fmt"
"net/url"
"time"
)
type ResetMessage struct {
To string
From string
Subject string
Text string
ExpiresAt time.Time
}
func renderReset(to, from, rawURL string, now time.Time, ttl time.Duration) (ResetMessage, error) {
if to == "" || from == "" || ttl <= 0 {
return ResetMessage{}, errors.New("recipient, sender, and positive expiry are required")
}
link, err := url.ParseRequestURI(rawURL)
if err != nil || link.Scheme != "https" || link.Host == "" {
return ResetMessage{}, errors.New("reset URL must be an absolute HTTPS URL")
}
expiresAt := now.Add(ttl).UTC()
return ResetMessage{
To: to,
From: from,
Subject: "Reset your routing-console password",
Text: fmt.Sprintf(
"A password reset was requested for your account.\n\nOpen %s before %s.\n\nIf you did not request this, ignore this email.",
link.String(), expiresAt.Format(time.RFC3339),
),
ExpiresAt: expiresAt,
}, nil
}
func main() {
now := time.Date(2026, 9, 22, 3, 0, 0, 0, time.UTC)
message, err := renderReset(
"dispatcher@example.eu",
"security@updates.example-logistics.com",
"https://accounts.example-logistics.com/reset?token=opaque-value",
now,
10*time.Minute,
)
if err != nil {
panic(err)
}
fmt.Printf("%s\nexpires=%s\n", message.Subject, message.ExpiresAt.Format(time.RFC3339))
}
Run it with go run main.go. The date makes the example deterministic; it is test data, not a claim about a production incident. In a real handler, generate the opaque token with the application's established authentication component and avoid logging either the token or the full reset URL.
There is a deliberate omission here: HTML. Start with a readable text part, then add reviewed HTML without moving either version into a provider console. That keeps a formatting change in the same code-review path as the authentication flow.
Step 2: Put one contract in front of delivery
The contract should describe what the application needs, not everything a provider sells. A reset sender needs a reviewed message and a stable operation identifier. It does not need a vendor template ID in business logic. My first pass would be to stop at that interface; the more useful preventative artifact, though, is a runnable adapter that proves the failure behavior too.
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(value string, attempt int, now time.Time) time.Duration {
if seconds, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if deadline, err := http.ParseTime(value); err == nil && deadline.After(now) {
return deadline.Sub(now)
}
return time.Second << attempt
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
payload := []byte(os.Getenv("INFRAI_EMAIL_REQUEST_JSON"))
if key == "" || len(payload) == 0 || !json.Valid(payload) {
panic("set INFRAI_API_KEY and a valid INFRAI_EMAIL_REQUEST_JSON")
}
operationSeed := os.Getenv("RESET_OPERATION_SEED")
if operationSeed == "" {
panic("set RESET_OPERATION_SEED to the stable reset operation identifier")
}
sum := sha256.Sum256([]byte(operationSeed))
idempotencyKey := "password-reset-" + hex.EncodeToString(sum[:])
endpoint := url.URL{
Scheme: "https",
Host: strings.Join([]string{"api", "infrai", "cc"}, "."),
Path: "/v1/email/send",
}
client := &http.Client{Timeout: 10 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
panic(fmt.Sprintf("email send failed: status=%d body=%s", resp.StatusCode, body))
}
delay := retryDelay(resp.Header.Get("Retry-After"), attempt, time.Now())
select {
case <-time.After(delay):
case <-ctx.Done():
panic(ctx.Err())
}
}
}
The request JSON comes from the current public capability schema rather than fields copied into this article and allowed to rot. RESET_OPERATION_SEED must identify the original reset operation, not an individual network attempt. Run the file with go run main.go; application code should render the message first, serialize the schema-valid request, and pass both environment values through its secret and job systems.
The idempotency key is the retry identity, so the application never generates a fresh value merely because a request timed out. Also cap retries by the remaining token lifetime. Sending a perfectly delivered link after it expires is an operational success and a user failure.
Keep the adapter boring. It sets an explicit method, uses bearer credentials from the environment, rejects non-success responses with useful context, and backs off on HTTP 429 while honoring Retry-After. Infrai's relevant advantage here is one plain REST API under one key, so the application contract can stay put while the vendor behind the capability moves. Suppression checks are available there as well, while delivery and bounce event follow-up is polling-based rather than webhook-driven.
That polling constraint matters. If the response team requires immediate pushed delivery events, this is the wrong fit; choose a provider whose documented event model meets that requirement. Do not build a busy polling loop and call it monitoring.
Step 3: Compare ownership, not feature counts
Resend, Postmark, Amazon SES, and Mailgun are real alternatives, but a fair selection cannot be reduced to a stale price row. Send the same reviewed fixture through a small proof of concept and score the behavior your responders will actually inherit.
| Option | Template-ownership choice to test | Operational question before adoption |
|---|---|---|
| Resend | Application-rendered content or provider-managed templates | Can a provider swap preserve variables and message text without an emergency edit? |
| Postmark | Application-rendered content or templates managed through its product | Do event retention and webhook handling meet the incident-response requirement? |
| Amazon SES | Application-rendered content or SES templates | Which region, identity, event destination, and account limits are part of the runbook? |
| Mailgun | Application-rendered content or stored templates | Which region and event-delivery behavior are covered by the selected account configuration? |
| Infrai | Application-rendered content behind one REST contract | Is polling for email events acceptable, and can spend analysis work without tag-aggregated cost reporting? |
This table is a test plan, not a claim that the rows are equivalent. Read the linked product documentation, validate the current contract and region terms, and record the evidence in the decision. Pricing should be checked at procurement time because it changes; it is not the architecture.
The practical appeal of the last option is that swapping the vendor behind the capability need not change application code: the contract stays put while the adapter or routed implementation moves. A second advantage is suppression management, which can stop repeated attempts to blocked or bounced recipients. Its limitations and trade-offs are equally concrete: no webhook event push, no SMTP relay, no hosted email OTP endpoint, and no tag-based cost aggregation API. It is not suitable for a reset flow with a strict, push-based delivery escalation; choose Postmark, Resend, Amazon SES, or Mailgun only after verifying that the selected product's current event behavior, region, and contract satisfy that requirement. For a simple custom-domain welcome-email path, polling may be acceptable.
Template ownership is the sharper divider. Provider-hosted templates can be convenient when non-developers must publish content independently and the organization accepts provider-specific review and rollback. Application-owned templates are a better default when exact authentication wording, deterministic tests, and rapid provider replacement outweigh that convenience.
Step 4: Test the page before trusting the graph
A useful preproduction test is small: render a known fixture, send it through each candidate adapter, assert that retry identity is stable, and verify that an expired token is never sent. Then test suppression behavior and the event collection path. Record a message ID, provider, operation ID, and coarse outcome in logs, but never the token or reset URL.
The alert should be based on the user journey and expiry budget. A queue growing while tokens age is actionable; a decorative delivery-rate panel with no threshold or owner is not. Ask the uncomfortable question during review: what page fires, and how much valid token lifetime remains when it does?
One trap deserves a short line. Do not fail open around suppression.
The adapter tests should also cover HTTP 429, ambiguous timeouts, malformed provider responses, and terminal client errors. For polling-based events, use bounded backoff and a durable cursor, then stop polling after the outcome or retention window makes further work pointless. Those mechanics belong in the adapter and worker, away from the reset-token issuer.
Where this design does not fit
Do not force application-owned templates onto a communications team that needs to edit campaigns without code deployment; separate marketing mail from authentication mail instead. Do not choose a polling-only event path when the incident objective requires immediate push. And do not present a pending domestic email vendor as evidence for mainland-China compliance: the legal and vendor-readiness review must stand on current contracts and deployed capability.
For the bounded logistics reset case, the decision is straightforward. Own the security template and expiry semantics, make delivery an adapter, and rehearse one replacement before production. A provider comparison then becomes evidence you can act on at 3 a.m., rather than another dashboard you hope is telling the truth.
Top comments (0)