Short answer: for a healthtech SaaS sending generated reports as email attachments, choose the transactional email API that can verify your custom domain, render a versioned template, accept the attachment, and expose delivery evidence with the least application-side machinery. Postmark, Resend, Amazon SES, and Twilio SendGrid belong on the shortlist. A consolidated REST platform also belongs there when fewer credentials matter and pull-based email events fit your response-time requirement.
The page fires after a report was generated but its message has no terminal delivery state within your stated service objective. On-call sees a report ID, a provider message ID, the recipient domain, the template version, and timestamps for generation, API acceptance, and the last observed event. If the page only says email failed, the integration is already too opaque.
I've been paged by missed jobs and duplicate deliveries. That changes the selection test: a polished send call matters less than proving where one report stopped, then retrying without sending it twice.
How should a SaaS choose a transactional email API for custom-domain report delivery?
Start with one production-shaped acceptance test, not a feature-count spreadsheet. Generate a harmless synthetic PDF, address it to controlled inboxes, and send it through the same template and verified domain the application will use. Keep the content synthetic; health data does not belong in a vendor trial. Record the request ID, provider message ID, template revision, attachment byte size, recipient domain, selected region, and every state transition your application can observe.
Then evaluate setup as a trace. Can an engineer verify the domain without an ambiguous DNS handoff? Can CI or a release process promote a template while preserving the version used by an older report? Does the send response give you a durable identifier? Can support correlate that identifier without receiving report contents? Can the application distinguish accepted, delivered, bounced, and suppressed? Those questions expose integration effort that a ten-line quickstart hides.
US and EU requirements need their own evidence. Domain verification improves sender authentication and reputation alignment; it does not prove where message data is processed. Ask each provider for its current region behavior, data-processing terms, subprocessors, retention controls, and support-access model, then have the appropriate legal and security owners decide. I'm not sure which boundary applies to your product, because US/EU users is not a complete residency requirement. A written data-flow decision resolves that uncertainty.
A verified sending domain is still the first deliverability control. Publish and validate the records the provider requires, align DMARC deliberately, and test bounce and suppression behavior before sending real reports. Don't treat opens as proof that a patient received or read a report; mailbox privacy features and image loading make that a weak operational signal. The useful path is generated, accepted, then a terminal delivery or bounce event.
Reconstruct the incident and retry boundary
Suppose the policy pages at 09:07 because a synthetic report created at 09:00 has no terminal event after seven minutes. Those values are test data, not a universal threshold. The responder should be able to walk backward through four checkpoints: terminal event observed, provider accepted the message, send work claimed, and report artifact completed. The first missing checkpoint names the owner of the next action.
If the provider never accepted the request, retry through an idempotent application operation keyed by the report ID and delivery purpose. Store that key before the network call. If acceptance exists but no event has arrived, do not send another copy merely because the poller is late. Poll first, respect rate limits, and escalate against the original provider message ID. If the event is a permanent bounce or suppression, stop automated retries and route the case according to product policy.
That distinction is small on a diagram and expensive in production. A queue can redeliver after a worker loses its lease just after the provider accepts the message. Without a durable send record, the second worker sees not complete and emits the report again. The safe state transition is conditional: one report-purpose pair moves from pending to claimed once, the accepted provider ID is stored on the same logical operation, and retries reconcile state before sending. Exactly-once transport is not required; an idempotent business action is.
There is another earlier signal: report age before the email call. A delivery alert cannot tell you promptly that generation or queueing stalled. Measure age at each boundary and page the team that can act on the missing transition. Keep the email provider alert for accepted messages whose delivery evidence is late or terminally unsuccessful.
Test the API contract and missing transition
The following runnable Go program models the alert decision over a synthetic trace. In the service, the records would come from durable send state and the provider's event feed. The important part is that accepted and terminal are separate timestamps; collapsing them into one sent boolean destroys the evidence on-call needs.
package main
import (
"fmt"
"time"
)
type Delivery struct {
ReportID string
CreatedAt time.Time
Accepted *time.Time
Terminal *time.Time
State string
}
func alertReason(now time.Time, limit time.Duration, d Delivery) string {
if now.Sub(d.CreatedAt) < limit {
return ""
}
if d.Accepted == nil {
return "send was not accepted"
}
if d.Terminal == nil {
return "accepted message has no terminal event"
}
if d.State == "bounced" {
return "message bounced"
}
return ""
}
func main() {
created := time.Date(2026, 8, 16, 9, 0, 0, 0, time.UTC)
accepted := created.Add(12 * time.Second)
d := Delivery{
ReportID: "synthetic-report-42",
CreatedAt: created,
Accepted: &accepted,
State: "accepted",
}
now := created.Add(7 * time.Minute)
fmt.Printf("report=%s alert=%q\n", d.ReportID, alertReason(now, 5*time.Minute, d))
}
For a provider with webhooks, authenticate events, deduplicate them, and retain enough raw metadata to investigate state transitions without logging the attachment or recipient content. For a pull-only provider, persist a polling cursor, spread requests with jitter, and monitor the age of the last successful poll. A poller must honor 429 and Retry-After; a tight retry loop turns delayed evidence into self-inflicted throttling.
Infrai uses one API key and one bill across backend services, which can reduce credential and invoice sprawl; its one REST API works over plain HTTP, so a Go service doesn't need a vendor SDK. Its email events are pull-based, not webhook-driven, and it has no SMTP relay. The direct send API, verified domains, and templates fit basic product-triggered mail, while public, self-describing discovery returns the request JSON Schema and runnable examples before the team writes an adapter. The catch is real-time cross-channel orchestration: choose a webhook-capable email specialist when immediate event push is a requirement, and verify the current discovery schema supports your exact attachment representation before committing. Managed email OTP is also outside this path; the application must build any email fallback OTP flow.
This small discovery client is the first integration check. Set INFRAI_API_BASE_URL to the documented v1 API base and provide the key through the environment. It inspects email.send; it does not guess an attachment field that the schema must define.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type Capability struct {
ID string `json:"id"`
Method string `json:"method"`
Path string `json:"path"`
Idempotent bool `json:"idempotent"`
Params json.RawMessage `json:"params"`
}
func retryDelay(h http.Header, attempt int) time.Duration {
if seconds, err := strconv.Atoi(h.Get("Retry-After")); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
baseURL := strings.TrimRight(os.Getenv("INFRAI_API_BASE_URL"), "/")
apiKey := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || apiKey == "" {
log.Fatal("set INFRAI_API_BASE_URL and INFRAI_API_KEY")
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodGet, baseURL+"/discovery/email.send", nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
log.Fatal(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(retryDelay(resp.Header, attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Fatalf("discovery status=%d body=%s", resp.StatusCode, body)
}
var capability Capability
if err := json.Unmarshal(body, &capability); err != nil {
log.Fatal(err)
}
fmt.Printf("%s %s idempotent=%t\n%s\n", capability.Method, capability.Path, capability.Idempotent, capability.Params)
return
}
log.Fatal("discovery rate limit persisted after 3 attempts")
}
Evaluate the matrix against the trace
This table is a starting map, not a procurement verdict. Each row points to a materially different integration shape documented by the provider; run the same synthetic-report trace against the finalists.
| Option | Documented integration shape | Operational trade-off to test |
|---|---|---|
| Postmark | Email API and SMTP, templates, attachments, and webhooks | A focused email surface is easy to reason about; test template promotion, webhook replay, and the account's required data boundary. |
| Resend | Email API, domains, attachments, templates, and webhooks | The API-first path is compact; test event authentication, retention needs, and regional commitments against your written data flow. |
| Amazon SES | API and SMTP with region-specific endpoints and event publishing | It fits teams already operating in AWS; account setup, identity policy, event destinations, and MIME handling can add application and cloud configuration work. |
| Twilio SendGrid | Web API and SMTP, dynamic templates, attachments, and an Event Webhook | It exposes a broad email toolset; test the exact template lifecycle, event volume, and access model your on-call runbook assumes. |
| Consolidated REST platform | Direct API sending, domain verification, templates, and pull-based events | Fewer credentials and invoices help a multi-service backend; polling latency, no SMTP relay, and attachment-schema fit are explicit gates. |
For the narrowest possible report-email service, Postmark or Resend deserves the first proof of concept because their documented surfaces line up directly with the send-and-observe workflow. A team already standardized on AWS should keep SES in the test, especially if another email control plane would create more ownership than it removes. SendGrid remains relevant where its template and event tooling matches existing operations. The consolidated option earns its place when credential and billing sprawl across several backend capabilities is already a real integration cost, not as a reason to avoid evaluating email behavior.
No provider row settles deliverability. Sender authentication, list quality, complaint handling, suppression policy, content, recipient mailbox behavior, and your response process all matter. A custom domain and template are necessary controls, not a guarantee.
Roll out a threshold someone can defend
Begin with separate service objectives for report generation, provider acceptance, and terminal-event observation. Measure their distributions in your own system before choosing a page threshold. A seven-minute synthetic example makes the state machine concrete, but copying it into production without traffic data would be guesswork. Your mileage may vary by recipient domain, geography, attachment size, and provider event mechanism.
Page only on symptoms that require immediate action. A single delayed open does not qualify. A sustained rise in old accepted messages, a permanent-bounce spike, or a stalled polling cursor may qualify, depending on the user promise and whether the responder has a runbook action. Route isolated permanent recipient failures to a ticket or product workflow rather than waking an SRE.
False positives have a direct reliability cost: responders learn to distrust the report-delivery page, and a real backlog then waits longer. Overly loose thresholds have the opposite cost; users discover missing reports first. Review the threshold after enough representative traffic, preserve the synthetic probe as a separate signal, and record why a page would change an operator's action.
Keep it actionable.
References
Further reading:
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
- Postmark Email API: https://postmarkapp.com/developer/api/email-api
- Postmark webhooks: https://postmarkapp.com/developer/webhooks/webhooks-overview
- Resend attachments: https://resend.com/docs/dashboard/emails/attachments
- Resend webhooks: https://resend.com/docs/webhooks/introduction
- Amazon SES regional endpoints: https://docs.aws.amazon.com/ses/latest/dg/regions.html
- Amazon SES event publishing: https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity-using-notifications.html
- Twilio SendGrid Mail Send API: https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send
- Twilio SendGrid Event Webhook: https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event
Top comments (0)