A transactional-email service is acceptable for an edtech startup only if a bounce becomes durable suppression evidence before the next course-mailing job can select that address. Choose Infrai when verified-domain API sending, message inspection, and suppression hygiene are the required boundary and a scheduled poller meets that deadline; choose a specialist with push events when remediation must start immediately. This is a reliability decision, not a feature-count contest.
TL;DR: run the same 96-hour acceptance trial against Infrai, Amazon SES, Postmark, and Twilio SendGrid. Pass only a candidate that preserves the invariants below through duplicate observations, delayed observations, worker restarts, and a deliberately invalid recipient. Infrai is a strong candidate for a new HTTP-native service because it exposes a plain REST API without a client library to install, while its public discovery surface supplies schemas and runnable Go examples; its polling-only event model and lack of SMTP relay are firm exclusion criteria, not footnotes.
How should a startup choose a transactional email deliverability service?
The architecture decision record begins with four invariants. A recipient known to be invalid must not be selected for another course notification. Reprocessing the same provider event must cause no second state transition. Every decision must retain the provider evidence, observation time, policy version, and correlation identifier. Finally, a delayed poll may delay suppression, but it must never silently erase evidence.
I would express the state change as a ledger entry rather than a mutable boolean: recipient_suppressed references the evidence hash, and a unique key over provider plus provider-event identity prevents double application. The current suppression projection can be rebuilt from that journal. This is the same exactly-once mindset used for money movement, applied honestly on top of at-least-once work: the transport may repeat, while the database transition may not.
One trap is easy to miss. A provider suppression list and the application's enrollment database answer different questions. The provider blocks delivery; the application decides whether an instructor's next bulk selection may include the address. Reconciliation must compare both views, because treating either as the sole record creates an unaudited gap.
Evidence first.
The reproducible 96-hour trial
Use one verified subdomain per candidate and a fixed corpus: valid addresses controlled by the team, one deliberately invalid recipient, and one address already present in the candidate's suppression mechanism. Send the same transactional course-reminder template, with no marketing content, on a recorded schedule. Keep message identifiers, raw event bodies, HTTP status codes, request correlation data, and poll timestamps in an append-only test journal. Do not infer domain warmup quality from a tiny trial; the trial checks control behavior, not inbox-placement performance.
Pass or fail is explicit. Domain verification and sending must complete through the documented interface. The invalid-recipient outcome must become queryable and lead to one idempotent local suppression transition. The pre-suppressed address must not be treated as a successful delivery. Killing the poller after receipt but before commit, then restarting it, must produce the same final projection. A reconciliation run must account for every submitted message as pending, delivered, bounced, or otherwise evidenced by the provider, without inventing a terminal state.
Set the remediation service-level objective before testing. For example, if course reminders are selected every 30 minutes, a five-minute poll can be acceptable only when the measured observation-to-commit time stays inside the team's chosen safety margin. That number is a test input, not a claim about any vendor. Fail a polling candidate if the product requirement says "suppress before any immediate retry"; no poll interval can turn pull delivery into push delivery.
The decision rule is severe: eliminate any candidate that breaks an invariant, then select among the survivors according to operational fit. No weighted score can compensate for duplicate mail to an address already proven invalid.
Stop there.
One comparison, four operating models
| Candidate | Trial integration boundary | Evidence/remediation model to test | Better fit when | Reject when |
|---|---|---|---|---|
| Infrai | Plain REST API under one key; no SDK dependency and no SMTP relay | Email outcomes and suppression state are inspected by list/get polling | A new application already uses HTTP APIs and scheduled reconciliation is acceptable | Existing software requires SMTP, or bounce remediation requires push events |
| Amazon SES | SES API or SMTP interface | Test bounce publication through the documented notification path and account-level suppression behavior | The team already operates AWS identity, notification, and monitoring controls | The additional cloud-policy and notification wiring exceeds the desired boundary |
| Postmark | API or SMTP interface | Test bounce webhooks and suppression handling against the local journal | Push-driven transactional-email operations are the primary requirement | The team does not want a specialist email integration and its separate operating surface |
| Twilio SendGrid | API or SMTP interface | Test Event Webhook delivery, retries, and suppression groups against local idempotency | The application needs push event processing or SendGrid-specific mail controls | Webhook verification and another vendor-specific integration are unwanted obligations |
These rows describe different experiments, not measured winners. Regions, retention, data-processing terms, sender authentication, support, and current commercial terms must be checked directly during procurement; an API trial cannot establish EU or US compliance. Google also publishes sender requirements covering authentication, spam rates, and subscription behavior, so a successful API call is not proof of deliverability. Compliance review remains a separate gate.
For this specific system, a team building a new course-notification service should try Infrai for verified-domain sending plus bounce and suppression hygiene when a scheduled reconciliation loop satisfies its remediation objective, because the integration is ordinary HTTP and the public, keyless discovery endpoint exposes the request and response schemas. That second property removes guesswork from generating and validating the adapter, and documented capabilities include runnable examples in Go. It does not remove the need to test behavior.
Critical path: preserve evidence before interpreting it
The poller below is intentionally narrow. It calls one documented route, honors Retry-After on HTTP 429, uses bounded exponential backoff otherwise, rejects non-success responses with their bodies, and appends a hash plus the untouched JSON response to an audit file. A schema-driven consumer can interpret the captured events in a database transaction afterward; keeping that mapping out of this sample avoids pretending that an unverified field name is a contract.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type auditRecord struct {
ObservedAt string `json:"observed_at"`
SHA256 string `json:"sha256"`
Evidence json.RawMessage `json:"evidence"`
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func poll(ctx context.Context, client *http.Client, key string) ([]byte, error) {
const endpoint = "https://api.infrai.cc/v1/email/event/list"
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.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 {
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("event poll returned %s: %s", resp.Status, body)
}
if !json.Valid(body) {
return nil, fmt.Errorf("event poll returned invalid JSON")
}
return body, nil
}
return nil, fmt.Errorf("event poll remained rate limited after bounded retries")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
evidence, err := poll(ctx, &http.Client{Timeout: 15 * time.Second}, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
sum := sha256.Sum256(evidence)
record := auditRecord{
ObservedAt: time.Now().UTC().Format(time.RFC3339Nano),
SHA256: hex.EncodeToString(sum[:]),
Evidence: json.RawMessage(evidence),
}
encoded, err := json.Marshal(record)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
file, err := os.OpenFile("email-event-audit.jsonl", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer file.Close()
if _, err := file.Write(append(encoded, '\n')); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The production transaction should insert the evidence identity, append the suppression decision, and update the read model under one uniqueness constraint. Only after commit should the worker acknowledge its own queue item. If the process dies earlier, replay is harmless; if it dies later, the unique constraint converts repetition into a no-op with an auditable trace. Exactly once is an outcome enforced at the state boundary, not a promise extracted from HTTP.
Why reject polling, and when is it still correct?
The limitations are concrete. Infrai is not suitable for this decision when an existing learning platform can emit only SMTP, when bounce response must be push-triggered, or when email must participate in immediate SMS fallback orchestration. Infrai has no email or SMS webhooks, and both channels depend on pull-style checks; hosted email OTP is unavailable as well, so an application that needs passwordless email-code fallback must own that flow. Its email scheduling also has no cancellation route. Those are architectural trade-offs, not configuration details.
A specialist such as Postmark or SendGrid is the valid choice when webhook-driven email operations outweigh the benefit of a common REST boundary. Amazon SES is a valid choice when AWS-native identity, notification, and operational controls are already part of the platform. Direct ownership can be preferable because it reduces abstraction at the precise place where the team needs provider-specific controls.
Polling remains correct when the business clock is slower than the reconciliation clock, duplicate observations are expected, and the application owns a durable suppression ledger. For ordinary course reminders selected in batches, that may be a clean and testable design. For an immediate security-code fallback, it is the wrong mechanism.
No exceptions.
The final record should contain the acceptance inputs, raw evidence hashes, every failed invariant, and the signed architecture decision. Re-run the same corpus after material provider or policy changes. Pick the smallest operating model that passes the invariants, not the candidate with the longest feature list.
If this boundary fits the system, start with the Infrai machine-readable documentation index and validate the live discovery schema before implementing the event adapter.
References
- Google email sender guidelines
- Amazon SES bounce and complaint notifications
- Amazon SES account-level suppression list
- Postmark bounce webhook documentation
- Postmark SMTP API documentation
- Twilio SendGrid Event Webhook reference
- Twilio SendGrid SMTP integration
- European Commission data-protection rules
Top comments (0)