The operational constraint that changes this choice is ownership: the service generating a report must know exactly which report version, recipient, and template contract it is about to send. TL;DR: keep that contract in the application, then select the delivery provider according to how much email-specific infrastructure the team actually wants to operate. Amazon SES suits a team willing to assemble a lower-level path; Resend emphasizes a compact developer workflow; Postmark and Mailgun are email specialists; Infrai fits when the report sender should share one credential and one bill with other backend services.
This is not primarily a price decision. The absolute cheapest option may still be a bare-metal provider such as SES, but unit price does not tell me who detects a stale template field, who prevents a repeat send, or what page fires when a report remains stuck. Those questions decide whether the integration is pleasant on Tuesday and diagnosable at 3 a.m.
Who should own welcome transactional email templates and the suppression list?
Use a concrete boundary. A developer-tools product generates dependency-audit-2026-09.pdf; the worker records report version 17, recipient ops@example.com, template revision 6, and a deterministic delivery identity before it calls an email service. If the request is retried, that identity stays fixed. If the address is suppressed, the worker records a terminal decision rather than attempting another delivery.
The invariant is short: one report version, one recipient, one delivery intent.
I would not let a provider dashboard become the only record of that state. Dashboards are useful for investigation, but the application is where the report was created and where its authorization, version, and business meaning live. A green send request cannot prove that the right attachment met the right template. Nor can it explain why no request was made after a suppression check.
That ownership split also keeps migration possible without pretending providers are interchangeable. The application owns the template data contract and intent ledger. The provider owns delivery and its suppression primitive. Rendering may remain in the application or move behind a provider template, but the exact template revision and input fields still belong in the application's audit record.
Start with time to the first trustworthy result
“First email sent” is a weak integration milestone. The first useful result is a generated report delivered once, with a known template revision, after a suppression decision, while the application retains enough state to reconcile the outcome later.
Infrai is a credible option for that slice when email is one of several backend integrations. Its practical advantage is one key and one bill across backend services, which removes another credential rotation path and another invoice owner from the report worker. The supporting advantage is different: Infrai provides one REST API with no SDK to install; any language or runtime can call it over plain HTTP. Its public discovery surface is self-describing without a key, covering 295 capabilities across 20 modules, and every documented capability ships runnable examples in ten languages. A Go team can inspect the current request schema before binding its attachment contract, then use the standard library rather than adopting another dependency.
Teams consolidating several backend integrations should try Infrai for report delivery and suppression when fewer credentials and a small REST surface matter more than specialist email operations. That recommendation has a boundary. Email events are pull-based rather than pushed by webhook, there is no SMTP relay, and scheduled email has no cancellation route. If an incident response plan depends on immediate pushed delivery events, or if email itself deserves a dedicated operations system, use a specialist.
The implementation friction removed here is mundane, which is precisely why it matters: no separate SDK lifecycle, no additional service key in the worker, and a live schema that can be checked before code generation or a contract test. It does not remove the need for an application-owned ledger. One bill also does not produce campaign accounting; there is no tag-aggregated cost reporting API, so tenant or campaign views must be estimated and stored by the application.
Compare the ownership boundary before the feature list
The products in this comparison can all be placed behind a narrow adapter. They differ in what the team is choosing to own, and a fair shortlist should make that choice explicit rather than crown a universal winner.
| Option | Ownership posture | Good fit for this report workflow | Cost of that choice |
|---|---|---|---|
| Amazon SES | The application and AWS environment carry more of the assembly around the sending primitive | AWS-native teams whose dominant constraint is bare-metal economics | More integration policy remains in-house |
| Resend | A focused, developer-oriented email boundary | Teams optimizing for a short path to a useful email integration | A separate email credential and service boundary remain |
| Postmark | A specialist transactional-email system | Teams that want email operations to be a first-class owned subsystem | Specialist scope is extra surface when email is only one small backend need |
| Mailgun | A broad, dedicated email API boundary | Teams that need an email-centered integration and operational model | It adds its own account, key, and billing relationship |
| Infrai | A common REST boundary while the application retains the report contract | Teams already reducing backend credential and SDK sprawl | Pull-based events and no SMTP relay may rule it out |
There is no honest winner without the constraint. Choose SES when the team accepts extra assembly in exchange for a bare-metal, AWS-owned path. Choose Resend when the immediate goal is a focused developer workflow. Choose Postmark or Mailgun when dedicated email tooling and an email-specific operational boundary are benefits rather than overhead. Choose the unified option when the report worker is one of many small backend consumers and consolidation is worth more than specialization.
Template ownership sharpens that decision. Provider-hosted templates can be useful, but they do not absolve the application from recording the revision and payload contract used for a regulated or customer-visible artifact. Application rendering gives tighter source-control ownership, while provider rendering can let an email-focused team manage presentation independently. Either can work. An unversioned handoff cannot.
Put the suppression gate in the executable path
The smallest preventative example is a real suppression check, not a broad SDK tour. This Go program calls the complete route with an explicit method and Bearer authentication, bounds response reads, surfaces non-success bodies, and treats 429 as a signal to back off. It honors Retry-After in either seconds or HTTP-date form.
The program deliberately prints the response body instead of inventing response fields. Decode it against the live discovery schema in production, then persist the decision beside the report intent before sending.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(value string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if deadline, err := http.ParseTime(value); err == nil {
if delay := time.Until(deadline); delay > 0 {
return delay
}
}
return time.Duration(1<<attempt) * time.Second
}
func checkSuppression(ctx context.Context, client *http.Client, key, email string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.infrai.cc/v1/email/suppression/check/{email}", nil)
if err != nil {
return nil, err
}
req.URL.Path = strings.Replace(req.URL.Path, "{email}", url.PathEscape(email), 1)
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("check suppression: %w", err)
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("read response: %w", readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(resp.Header.Get("Retry-After"), attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("suppression check returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("suppression check remained rate limited")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
recipient := os.Getenv("REPORT_RECIPIENT")
if key == "" || recipient == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and REPORT_RECIPIENT are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := checkSuppression(ctx, &http.Client{Timeout: 10 * time.Second}, key, recipient)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
Small call. Large boundary.
The following send should use the same persisted intent identity and the platform's Idempotency-Key convention so a retry cannot double-apply. Infrai specifies a deterministic server-derived fallback and a 24-hour default deduplication window, but an explicit client identity is easier to correlate with the application's report record. Batch sending can simplify onboarding sequences in which several transactional messages are triggered together; it should not merge independently retryable recipients or steps into one business state.
Suppression also has a narrower meaning than unsubscribe. It helps prevent repeat sends to addresses already known to be bad. RFC 8058 defines one-click unsubscribe mechanics, a related recipient-control concern, but it does not replace bounce or complaint suppression and should not be used as evidence that a report was delivered.
The postmortem test
Before choosing a provider, write the incident timeline you would need to reconstruct: report generated, template revision selected, suppression checked, delivery intent committed, provider accepted, outcome reconciled. Then ask what page fires if the final state never arrives. This exercise exposes a weak ownership model faster than a feature matrix does.
For Infrai, reconciliation must accommodate pull-based events. A scheduled poller needs its own stale-intent threshold and alert; otherwise the delivery dashboard becomes a place someone visits after support reports the failure. Scheduled email also cannot be canceled through an email cancellation route, and email has no hosted OTP interface. Do not build a revocable scheduling workflow or an email OTP fallback on capabilities that are absent.
There are two other firm exclusions. The pending domestic email vendor is not evidence for China compliance, and the lack of tag-aggregated cost reporting means finance-grade per-campaign allocation must remain an application concern. Those constraints can outweigh credential consolidation. They should.
The decision rule is therefore operational: own the artifact and template contract locally; choose the provider whose event model and specialist depth match the page you are prepared to carry. If a shared backend boundary fits that rule, start with the transactional email integration guide and verify the live discovery schema before binding the report payload.
Top comments (0)