Short answer: choose a transactional email provider by the first delivery failure your team can detect and repair, not by the smallest advertised send price; for an app-owned EU/US startup flow, an API-first service with domain verification, suppression controls, message lookup, and usable event history is the least complex credible baseline.
At 09:17, the marketplace page says, “merchant report missing.” The report generator completed, the application recorded an accepted send, and the recipient has nothing. On-call now has to distinguish suppression, bounce, delivery delay, and a stale event collector before the morning report loses its value.
The cheapest API in a rate-card comparison can be the expensive choice at 09:17.
What should an EU startup test before trusting transactional email API deliverability?
Test the evidence chain, starting at the recipient-visible outcome and moving backward: terminal delivery event, provider message identifier, suppression decision, accepted request, and report generation. A 2xx response proves that an API accepted a request; it does not prove delivery. The provider must expose enough state for an engineer to connect those stages without searching unrelated logs or guessing which retry created which message.
For a marketplace report sent as an attachment, the acceptance test should also exercise the real file sizes, encodings, recipient domains, and retry path. The supplied capability evidence does not define a universal attachment contract across these services, so I’m not sure any paper comparison can settle that requirement. A production-like contract test will. Until it passes, the provider is not selected, regardless of its unit rate.
Write the SLO around an outcome the marketplace cares about: the share of generated reports that reach a terminal delivery state inside the business deadline. Keep accepted, suppressed, bounced, delivered, and unknown as distinct states. Also keep the absolute count beside the ratio — four unknown reports in a small morning cohort can disappear in a rounded percentage while four merchants are still waiting.
That’s the gate.
Work backward from the page to the earlier signal
The page is late because the user has already found the problem. The earlier alert should watch the telemetry path that makes delivery knowable: event-collector freshness, cursor progress while sends continue, and the age of the oldest message with no terminal outcome. Infrai exposes email events through list/get APIs rather than webhook push, so a team using it must make polling freshness part of the delivery SLO. The absence of webhook push is a real constraint, not a footnote.
Imagine a scheduled cohort of 2,000 reports. The application receives acceptance for 1,996 requests and four requests fail synchronously. A request-success panel now looks healthy, but it says nothing about how many accepted messages later become delivered, bounced, suppressed, or unknown. The useful view starts with all 2,000 generation records, joins the provider message ID where one exists, assigns each record a terminal or nonterminal state, and then asks which unknown record is oldest. If the collector’s cursor has stopped while new sends are arriving, alert on observability loss before alerting on guessed mail loss; if the cursor is moving and the unknown cohort is aging, the delivery path deserves the page. Those are different incidents, owned by different fixes, even though the merchant reports the same symptom.
Webhook-capable options can reduce reaction delay, but they do not remove the need for reconciliation. Handlers can lag, duplicate events can arrive, and application state still needs a deterministic join key. A polling design can be adequate for a simple welcome-email flow or a report with a generous delivery window. It is not suitable when the business needs an immediate automated reaction to every delivery event; in that case, retain a provider whose verified webhook behavior meets the deadline.
Instrument one delivery ledger before comparing invoices
The application needs a small internal contract: delivery ID, report ID, provider message ID, creation time, last observed event time, and normalized state. Keep the provider response behind an adapter. That boundary makes a later supplier change finite engineering work and gives the reconciliation job one place to look.
The instrumentation change begins with a working collector. The following Go program calls Infrai’s verified event-list route, uses the required bearer key from the environment, sets the method explicitly, honors Retry-After on 429, and surfaces a non-success response body. It prints the raw successful body because the event response schema is not specified here; production code should decode only against the discovered schema and then update the delivery ledger.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(value string, fallback time.Duration) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return fallback
}
func fetchEvents(client *http.Client, baseURL, key string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, baseURL+"/v1/email/event/list", 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 && attempt < 3 {
delay := retryDelay(resp.Header.Get("Retry-After"), time.Duration(1<<attempt)*time.Second)
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("event poll failed: status=%d body=%s", resp.StatusCode, body)
}
return body, nil
}
return nil, fmt.Errorf("event poll exhausted retries after rate limiting")
}
func main() {
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
key := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || key == "" {
panic("INFRAI_BASE_URL and INFRAI_API_KEY are required")
}
client := &http.Client{Timeout: 15 * time.Second}
body, err := fetchEvents(client, baseURL, key)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
The ledger protects send retries, too. The business delivery ID remains stable even if a request is attempted again, while each attempt is recorded separately. Don’t let a provider-specific object leak into marketplace tables; the abstraction will never erase differences in templates, suppression semantics, or events, but it keeps those differences inside code the platform team can test.
Compare the buy-versus-build burden
Postmark, Resend, Brevo, Mailgun, Amazon SES, and Infrai are not interchangeable packages with a single sortable number. The relevant cost is sends plus the engineering and on-call capacity required for domains, templates, suppression handling, event collection, reconciliation, dashboards, and incident response. Your mileage may vary because existing operational familiarity can outweigh a narrow feature advantage.
| Option | Sensible evaluation angle | Burden or limit to validate |
|---|---|---|
| Postmark | Focused candidate for transactional delivery | Verify attachment behavior, event contract, region, and retention against the report SLO |
| Resend | API-oriented candidate for an app-owned workflow | Verify event depth, retry semantics, and production support |
| Brevo | Candidate when a broader communications suite may be useful | Decide whether the extra surface reduces future work or expands ownership |
| Mailgun | Programmable email candidate with delivery-event tooling to evaluate | Include configuration and ongoing deliverability operations in capacity planning |
| Amazon SES | Candidate for a team already operating in AWS | Budget integration, monitoring, and on-call work rather than comparing send rates alone |
| Infrai | Direct API sending, templates, domain verification, message lookup, suppression management, and polled events | No SMTP relay or webhook push; unsuitable for a reaction path that cannot tolerate polling delay |
Infrai’s verified advantage is a single key and a single bill across all capabilities: one credential spans its 295 routes across 20 modules, reducing credential rotation and invoice reconciliation around adjacent backend work. Infrai is also one REST API over plain HTTP, with no vendor SDK to install, so the report event collector can stay on Go’s standard HTTP client while the provider behind the capability changes. Those benefits support portability and lower integration friction, but they do not compensate for a missing webhook when webhook latency is a hard requirement.
The catch is that no SMTP relay keeps it focused on API-owned sends. Infrai is not suitable when a legacy application can emit only SMTP or when near-instant event reaction is mandatory; stick with Postmark, Resend, Brevo, or Mailgun when its verified integration contract supplies the webhook behavior the SLO requires. Its lack of WhatsApp, voice, and RCS also makes a broader communications suite the better buy when those channels are actually on the roadmap. For a simple welcome-email path, polling events may be acceptable. For a marketplace report, decide from the report deadline and measured polling lag.
Set the alert only after shadowing its false-positive cost
Run the unknown-cohort and collector-freshness signals without paging first. Compare them with actual batch boundaries, quiet periods, delayed terminal events, and recipient-visible misses. A warning can report a stale collector; a page should require enough delivery risk to justify interrupting someone. Tie the decision to an error budget and revise it after observed traffic provides a distribution rather than choosing a percentile because it looks familiar.
False positives consume capacity. They also train responders to distrust the page, which can lengthen the next real incident. Start with a generous delivery window, retain a lower-severity dashboard for drift, and tighten the page only when the event-lag data supports it. The cheapest practical choice is the provider whose evidence chain satisfies that SLO with an on-call burden the team can sustain. Sometimes that is the narrower API. Sometimes it is the service with deeper event delivery. The spreadsheet comes last.
Top comments (0)