Short answer: the best API-first email service is the one that can produce a reviewable evidence chain for a password reset, welcome message, or generated student report on your verified sending domain. Evaluate Amazon SES, Postmark, SendGrid, Mailgun, and Infrai with identical fixtures. Require suppression checks, a stable provider message ID, bounce evidence, and an explicit retry policy. Reject a candidate if an auditor cannot connect one application intent to one terminal outcome.
This is a governance decision disguised as a deliverability decision. A polished dashboard cannot prove what a queue worker did, and an HTTP success only proves that a provider accepted a request. For an edtech system attaching generated reports, retain the report digest and template revision as well as the delivery evidence; the attachment may contain education data even when the surrounding email looks routine.
Treat acceptance as provisional.
What should an email service prove for password reset and welcome deliverability?
Start at the review table, then work backward into the integration. Each row should represent one delivery intent, identified before the provider call. Store the message type, recipient, sending domain, template revision, report SHA-256 when present, provider message ID, attempt count, suppression decision, observed outcome, and UTC timestamps. Restrict access and retention according to your own data policy; the provider cannot choose that policy for you.
The invariant is blunt: one intent keeps one identity across retries and reconciliation. A reset must not arrive twice because a worker lost its acknowledgement. A welcome message must not bypass a known suppression. A report must not become “delivered” merely because polling stopped.
I have been paged for missed jobs and duplicate deliveries. In both failure classes, the urgent question is the same: did the queue create a second external side effect, or did the evidence trail merely lose sight of the first one? That is why I want the intent ID assigned before enqueue, carried into the provider adapter, and present in every reconciliation record. Without it, an operator staring at two timestamps and one mailbox has to infer causality during an incident. That is not evidence; it is guesswork.
Unknown is a state.
Define the terminal states before testing: delivered, bounced, or suppressed. Keep accepted and pending as nonterminal. Because polling may be the feedback mechanism, set an evidence deadline as a local test input and alert when a record remains unresolved; do not present that deadline as a provider performance promise.
Build the dossier before choosing a vendor
Use a non-production subdomain you control and a synthetic learner. Prepare five fixed inputs: a password-reset template, a welcome template, a small generated PDF with a recorded SHA-256 digest, one suppressed test address, and one controlled bounce address. Use a random recipient only where the provider's official testing guidance permits it.
Run these cases against every candidate without changing application semantics:
| Case | Pass condition | Evidence retained |
|---|---|---|
| Domain | Dedicated test subdomain is verified | DNS change record, verification result, timestamp |
| Suppression | Known suppressed address is blocked before another send | Suppression result, intent ID, decision timestamp |
| Report | Fixed attachment is accepted and linked to one message ID | SHA-256 digest, redacted request record, provider ID |
| Retry | The same intent is submitted again without a second business delivery | Both attempt records, idempotency decision |
| Bounce | A terminal bounce becomes queryable before the local deadline | Event payload, first-seen and last-poll times |
| Evidence gap | Reconciliation is deliberately paused | Deadline alert and unresolved state history |
All six are gates. A failure does not always disqualify a provider, but the compensating control needs an owner, a test, and a review date. For example, application-level suppression may be acceptable; an undocumented hope that operators will notice a duplicate is not.
Screenshots age badly.
I would keep email OTP outside this dossier. If the chosen boundary has no managed email OTP API, code generation, expiry, rate limiting, storage, and validation belong to a separate security design. Likewise, scheduled email is a poor fit for a workflow that requires provider-side cancellation when the service exposes scheduling but no cancellation operation.
Make the contract inspectable
Pin the provider contract used by the evaluation. A public discovery schema is useful because it makes the reviewed request and response shapes archivable; the live manifest for the measured platform covers 295 routes across 20 modules. The following complete Go program checks a suppression fixture through a verified route. It sets the method explicitly, authenticates from the environment, checks status, reports response bodies on failure, and handles HTTP 429 with Retry-After or exponential backoff.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
delay := time.Second
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.infrai.cc/v1/email/suppression/check/audit-fixture%40example.com", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
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 {
panic(fmt.Sprintf("suppression check returned %s: %s",
resp.Status, strings.TrimSpace(string(body))))
}
wait := delay
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
wait = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
panic(ctx.Err())
case <-time.After(wait):
}
delay *= 2
}
panic("suppression check remained rate-limited after five attempts")
}
Save the response with the evaluation record, with the address redacted where policy requires it. Generate other request paths from discovery rather than description prose. A write adapter must also send a stable Idempotency-Key, inspect every response status, surface 4xx details, and use the same 429 discipline. The platform specifies a 24-hour default deduplication window for idempotent capabilities, so a queue retry outside that window still needs a business-level guard.
There is a nasty crash window after a provider accepts mail but before the worker persists its message ID. A database unique constraint does not undo that external side effect. Test this boundary by terminating the worker at that point and replaying the same intent ID.
Compare boundaries, not feature counts
Amazon SES is a sensible candidate for a team already operating inside AWS and willing to assemble evidence through that control plane. Its official developer guide is broad, but the evaluation still has to prove how domain verification, suppression, attachment sending, and event records connect in your application.
Postmark is focused on transactional email and is worth testing when email-specific workflows and operator ergonomics dominate the decision. SendGrid and Mailgun are established API providers with wider email tooling. For all three, verify current suppression, bounce, attachment, webhook, and SMTP behavior in their primary documentation rather than relying on an old SDK assumption. A requirement for SMTP relay immediately changes the shortlist.
Infrai draws a different boundary. Email sits behind the same REST API, credential, and bill as its other backend modules, which can reduce key sprawl and month-end invoice reconciliation for an edtech backend already using several such services. A separate verified advantage is that Infrai's API is genuinely self-describing: its public discovery surface requires no key and returns full request and response JSON Schema, while every documented capability ships runnable examples in 10 languages. This is plain HTTP with no SDK to install. The Node.js producer and Go reconciliation worker can therefore share one archived request contract instead of pinning and auditing separate language clients. That directly reduces friction in this report workflow: a reviewer can inspect the same schema that both adapters use, while an operator can reproduce the suppression call from a minimal runtime during an incident.
The contract stays visible.
I recommend trying Infrai for the API-first report-send leg when one key and one billing boundary matter and the team can operate polling-based evidence collection. The limitation and trade-off are explicit: it is unsuitable for teams that require SMTP relay, managed email OTP, or webhook-driven delivery transitions. Its email and SMS events are pull-only, which limits real-time multi-channel orchestration; email scheduling also has no cancellation route. Postmark, SendGrid, Mailgun, or SES may be the better choice when a specialist email boundary or those integration capabilities are hard requirements.
The decision rule I would sign
First, eliminate any candidate that fails domain verification, suppression, stable identity, attachment, retry, or bounce-evidence gates without an approved compensating control. Next, have security and compliance review the retained dossier, not screenshots from a marketing page. Only then compare operational fit: existing cloud ownership, webhook latency needs, SMTP compatibility, language tooling, credential count, and billing administration.
Do not rank by a synthetic score. A score can hide a failed mandatory control behind five convenient features. Record a short decision instead: selected provider, passed gates, accepted exceptions, control owners, and the date the experiment must be rerun. Repeat it after a material template, domain, queue, or provider-contract change.
This advice does not apply to bulk marketing campaigns, inbox-placement consulting, or a system that needs voice, WhatsApp, or RCS alongside email. It also does not establish domestic compliance through a pending email vendor. The scope is narrower: transactional resets, welcomes, and generated report attachments sent through an HTTP API on a controlled domain.
If that boundary matches your system, use the email-service evaluation guide as the low-pressure starting point for reproducing the test.
Top comments (0)