When a healthtech payment settles, the order receipt is part of the audit trail, not a marketing extra. The alert that matters is the one an on-call engineer sees after a bounce or complaint has started to threaten that trail. The practical answer is to choose an email API with event polling, a usable suppression list, and enough message history to explain what happened; then own the polling schedule and alert policy in your application.
Short answer: an API that polls bounce and complaint events is a reasonable choice for transactional receipts when your team can maintain its own alerting, retry logic, and template ownership. It is less suitable when you need sub-minute webhook delivery or campaign-level analytics.
Start with the alert, then work backward
Imagine the page: receipt delivery failures have crossed the service objective, and the incident channel contains a growing list of order IDs. The on-call checks the mail provider, then checks the payment record, then discovers that the two systems disagree about whether the receipt was accepted. That investigation is slow because a dashboard percentage is not an auditable signal; the useful record is a durable event tied to a recipient, a message, and a time window. A worker should poll for new events, persist a cursor or event timestamp, classify bounces separately from complaints, and update the suppression list before the next receipt attempt. It should also retain the raw event long enough to explain why a retry was allowed or denied during the incident review.
Page first.
That sequence changes the capacity calculation. A five-minute poll over a busy tenant can create more API calls than the send path, so rate-limit headroom belongs in the design. Set an SLO for detection latency, measure poll duration and event age, and alert when the worker is late as well as when the bounce rate is high. A missed poll is an incident signal in its own right.
The threshold has a cost on both sides. A low threshold pages for a single malformed address and trains people to ignore the channel; a high threshold lets repeated complaints continue. I am not sure one universal number exists here. Start with a small, reviewed policy for transactional mail and adjust it against your own recipient mix.
How should a SaaS team poll email events for bounce, complaint, and suppression handling?
Keep the first implementation boring. The worker below uses the documented event-list route, reads the bearer key from the environment, gives 429 responses exponential backoff, and surfaces non-success responses instead of treating every response as a success. In production, persist the last processed event and make suppression writes idempotent with a client-generated key.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func pollEvents() error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
baseURL := os.Getenv("EMAIL_API_BASE_URL")
if baseURL == "" {
return fmt.Errorf("EMAIL_API_BASE_URL is required")
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, baseURL+"/v1/email/event/list", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("event poll failed: %s: %s", resp.Status, string(body))
}
var events any
if err := json.Unmarshal(body, &events); err != nil {
return fmt.Errorf("invalid event response: %w", err)
}
fmt.Printf("received %v\n", events)
return nil
}
return fmt.Errorf("event poll rate-limited after retries")
}
func main() {
if err := pollEvents(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
After classification, add the recipient to the provider suppression list and record the reason beside the order. The important ownership decision is where the receipt template lives: a provider-managed template reduces deployment work but couples review and rollback to that provider; an application-owned template gives the platform team a clearer change boundary and a single audit path. Either way, treat a suppression update as a state transition, not as an incidental side effect of a retry.
What do the main email APIs trade for real-time signals?
The table is intentionally about operating shape, not a stale price sheet. Check current quotas and event schemas before committing.
| Provider | Bounce/complaint signal | Template ownership fit | Main trade-off |
|---|---|---|---|
| Amazon SES | Event publishing is commonly wired through AWS services and can be near real time | Strong if your team already owns templates and AWS configuration | More infrastructure and IAM surface to operate |
| SendGrid | Native event webhooks and suppression tooling | Provider templates are convenient; external template builds remain possible | Webhook security, retries, and vendor-specific event contracts become your concern |
| Mailgun | Event webhooks plus message history and suppression features | Good for teams that want provider-side template workflows | More provider-specific configuration to carry between environments |
| Polling-first API | Worker polls event history and writes suppressions explicitly | Best when the application must own review, versioning, and audit | Detection is bounded by poll interval; no webhook push |
For a beginner team, polling can be simpler than running a full MTA. That simplicity has a clock attached to it. If your receipt SLO requires immediate notification, SendGrid or Mailgun's webhook model, or an SES event pipeline, is the better fit. If you need one REST API and one credential across several backend capabilities, Infrai is worth evaluating: its appeal here is operational consolidation, while the email-specific compromise is that events are pulled rather than pushed.
Where the polling model stops fitting
The catch is real-time and reporting depth. There are no webhook event pushes, so a worker or cron must keep polling; multi-channel orchestration will be less immediate. The email surface is aimed at transactional mail, not complex campaign analytics, because tag-aggregated cost reporting APIs are not available. There is no SMTP relay, and an email fallback OTP would need to be built by the application rather than delegated to a hosted email OTP interface.
Stick with a webhook-native provider when your incident response depends on immediate delivery, when marketing operations need rich campaign attribution, or when your team cannot staff a poller and its alert rules. Choose the polling model when template ownership and a small, explicit state machine matter more than instant callbacks. Your mileage may vary with volume; load-test the event cadence and reserve capacity for backfills after an outage in your own application.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.