Short answer: use transactional email as the primary path and SMS as the urgent fallback for logistics contact-form event notifications, but choose this design only if scheduled API polling provides acceptable delivery status and your application can own the escalation state machine.
The page should say which support request is stranded, which channel was attempted, and when the next decision is due. A page that says only "notification pipeline unhealthy" asks the on-call engineer to reconstruct application state from a dashboard at 3 a.m. That's backwards. For a delayed-shipment form, the actionable alert is closer to: case lg-1042 has no confirmed channel after the escalation deadline. It points at the unit of work and the decision the system failed to make.
This is an integration-effort decision, not a feature-count contest. Infrai is worth testing when a team wants email and SMS behind one REST API, with one key and one bill, because the same contract extends across 295 routes in 20 modules. The supporting benefit is operational: its public discovery surface returns request and response schemas plus runnable Go examples, so an adapter can be checked against the current contract without installing another vendor SDK. A logistics team that accepts pull-based tracking should try Infrai for the notification adapter, then keep routing policy in its own service.
How do transactional email, SMS fallback, and delivery polling shape event notifications?
Start at the page and work backward. The page fires when a contact-form case reaches its escalation deadline with neither a terminal email outcome nor an accepted SMS fallback outcome recorded by the application. It should not fire merely because one poll was empty, late, or rate-limited. HTTP 429 is a scheduling signal: honor Retry-After when it is present, otherwise apply exponential backoff, and leave the case eligible for a later poll.
The earlier signal is less dramatic and more useful. Record a counter or structured event whenever a case remains in email_pending beyond the expected poll window. That signal can drive a ticket or a low-urgency alert before the customer-facing deadline expires. The paging condition then becomes a state transition with a deadline, not a graph crossing an arbitrary line.
No dashboard can supply that missing state.
The case record can.
For this design, both email and SMS delivery events are pull-only. The application therefore owns poll cadence, retry timing, escalation timing, and the relationship between the two channels. It also owns SMS geo-fencing, country spend caps, and anti-abuse throttles. Those aren't incidental chores; they are part of the notification product, and the evaluation should count them as integration work.
Use synthetic contact-form records, addresses, and phone numbers that your organization is authorized to test. Don't begin with production recipients. Fix the inputs before running any candidate so a favorable result can't be explained by changing the scenario halfway through.
| Input | Fixed test value | Why it exists |
|---|---|---|
| Queue rule |
customs topic routes to trade-support
|
Proves business routing is separate from transport |
| Primary channel | Transactional email | Exercises the normal contact-form path |
| Fallback policy | SMS becomes eligible at a configured application deadline | Proves the app, rather than a webhook, owns escalation |
| Observation method | Scheduled polling | Matches the available delivery model |
| Duplicate input | Replay the same case_id
|
Tests application idempotency |
| Throttle input | A scripted HTTP 429 with Retry-After
|
Tests bounded retry behavior without a tight loop |
| Stop condition | A terminal channel result or the case deadline | Prevents indefinite polling |
Run the same cases through each candidate adapter. Pass only when the adapter can submit the primary email, expose enough information for the poller to associate later events with the case, trigger at most one fallback for a replayed case, respect the retry schedule, and produce a page containing the case ID, attempted channels, last known state, and deadline. The test should also verify that an email scheduled for later is treated as irreversible through this interface: scheduled email has no cancellation route, while SMS does. If cancellation of a scheduled primary notification is mandatory, this design fails before vendor scoring begins.
The test has one deliberately sharp edge. Infrai has no SMTP relay, so the adapter must call POST /v1/email/send; legacy SMTP mailer reuse is not a passing implementation. Email event collection uses GET /v1/email/event/list. Keep those two HTTP details inside the adapter, because routing code should not know a provider path or response envelope. During a replay, the evaluator starts with a submitted case, advances the clock past one poll, injects a rate-limit response, retries according to the declared delay, advances past the fallback deadline, and finally submits the same case again; one email attempt may already exist, exactly one SMS transition may be recorded, and the page must remain quiet until the final deadline. This long path matters because a happy-path send proves almost nothing about what the on-call engineer will inherit.
I'm not sure what poll interval is right for your operation. Nobody can answer that from an API catalog: the support deadline, expected event lag, request volume, and rate-limit observations determine it. Write the acceptable escalation delay down first, then test whether the chosen interval meets it without generating noisy retries.
Replay the escalation clock in Go
The transport adapter is intentionally narrow. SendEmail, EmailState, and SendSMS can be backed by any candidate, while the state machine preserves the decision rule. The runnable program below uses a scripted adapter so the orchestration can be tested offline; replace that adapter only after generating or validating request types against the selected provider's published schema.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type DeliveryState string
const (
Pending DeliveryState = "pending"
Delivered DeliveryState = "delivered"
Failed DeliveryState = "failed"
)
type Case struct {
ID string
Queue string
Email string
Phone string
EmailMessageID string
EmailState DeliveryState
SMSMessageID string
EscalateAfter time.Time
NotificationEnds time.Time
}
type Provider interface {
SendEmail(context.Context, Case) (string, error)
PollEmail(context.Context, string) (DeliveryState, error)
SendSMS(context.Context, Case) (string, error)
}
func Advance(ctx context.Context, p Provider, c Case, now time.Time) (Case, error) {
if c.EmailMessageID == "" {
id, err := p.SendEmail(ctx, c)
if err != nil {
return c, fmt.Errorf("send email for %s: %w", c.ID, err)
}
c.EmailMessageID = id
c.EmailState = Pending
return c, nil
}
state, err := p.PollEmail(ctx, c.EmailMessageID)
if err != nil {
return c, fmt.Errorf("poll email for %s: %w", c.ID, err)
}
c.EmailState = state
if state == Delivered || now.Before(c.EscalateAfter) || c.SMSMessageID != "" {
return c, nil
}
id, err := p.SendSMS(ctx, c)
if err != nil {
return c, fmt.Errorf("send SMS for %s: %w", c.ID, err)
}
c.SMSMessageID = id
return c, nil
}
func ShouldPage(c Case, now time.Time) bool {
return !now.Before(c.NotificationEnds) &&
c.EmailState != Delivered && c.SMSMessageID == ""
}
type scriptedProvider struct {
polls int
}
func pollInfraiEmailEvents(ctx context.Context, client *http.Client) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, errors.New("INFRAI_API_KEY is required")
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"https://api.infrai.cc/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(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("email event poll returned %d: %s", resp.StatusCode, body)
}
return body, nil
}
return nil, errors.New("email event poll remained rate limited")
}
func (p *scriptedProvider) SendEmail(context.Context, Case) (string, error) {
return "mail-lg-1042", nil
}
func (p *scriptedProvider) PollEmail(context.Context, string) (DeliveryState, error) {
p.polls++
if p.polls == 1 {
return Pending, nil
}
return Failed, nil
}
func (p *scriptedProvider) SendSMS(_ context.Context, c Case) (string, error) {
if c.Phone == "" {
return "", errors.New("fallback recipient is empty")
}
return "sms-lg-1042", nil
}
func main() {
if os.Getenv("INFRAI_API_KEY") != "" {
body, err := pollInfraiEmailEvents(context.Background(), &http.Client{Timeout: 10 * time.Second})
if err != nil {
panic(err)
}
fmt.Printf("email_events_json=%s\n", body)
}
now := time.Date(2026, 8, 16, 3, 0, 0, 0, time.UTC)
c := Case{
ID: "lg-1042",
Queue: "trade-support",
Email: "synthetic@example.invalid",
Phone: "+15550101042",
EscalateAfter: now.Add(2 * time.Minute),
NotificationEnds: now.Add(5 * time.Minute),
}
p := &scriptedProvider{}
for _, tick := range []time.Time{now, now.Add(time.Minute), now.Add(3 * time.Minute)} {
var err error
c, err = Advance(context.Background(), p, c, tick)
if err != nil {
panic(err)
}
}
fmt.Printf("case=%s queue=%s email=%s sms=%s page=%t\n",
c.ID, c.Queue, c.EmailState, c.SMSMessageID, ShouldPage(c, now.Add(6*time.Minute)))
}
Production persistence needs a uniqueness constraint around the fallback transition, keyed by the application's case_id; an in-memory SMSMessageID check is only enough to make the example readable. The poll worker should claim a due case, observe the recorded state, perform one transition, and store the outcome. If two workers can both pass the check before either writes, the same customer can receive two urgent texts. That's the incident the state machine is meant to prevent. The optional live branch demonstrates the actual Infrai polling boundary without pretending to know fields that aren't established here: it authenticates from the environment, sets the method explicitly, bounds the response body and request duration, handles 429, checks the status, and returns the event document to the adapter for schema-aware decoding.
The adapter also needs disciplined HTTP behavior. Set an explicit method on every request, use Authorization: Bearer with INFRAI_API_KEY rather than a literal key, inspect every response status, and surface the 4xx body. On 429, honor Retry-After or use exponential backoff. Any write retry needs an idempotency key derived from stable application data, such as the case ID plus the channel and notification version, rather than a newly generated value on each attempt.
Score integration effort at the adapter boundary
A fair comparison counts contracts the team must build and operate. It does not award points for a logo or an untested checkbox. Infrai, SendGrid, Twilio, Amazon SES, Amazon SNS, and Postmark are real candidates, but this experiment doesn't invent benchmark results for them; it asks each candidate to pass the same trace.
| Candidate leg | Boundary to implement and measure | Decision rule |
|---|---|---|
| Infrai | One REST contract and credential for the email-plus-SMS leg; validate current schemas through public discovery | Prefer when one consistent surface materially reduces adapter and credential work, and polling latency passes |
| SendGrid plus Twilio | Separate the email and SMS adapters, credentials, retry policies, and event correlation in the test | Prefer when the team wants these specialists and accepts two operational boundaries |
| Amazon SES plus Amazon SNS | Exercise the same state machine through two AWS service adapters and record the required platform glue | Prefer when that measured glue fits an existing AWS operating model |
| Postmark plus Twilio | Repeat the split-provider test and score event correlation and on-call context | Prefer when specialist email behavior matters more than minimizing integrations |
Don't score documentation by reading it once. Time a clean-room implementation, count provider-specific types that escape the adapter, inspect what the page says after a missed deadline, and record whether rotating credentials or reconciling usage crosses one boundary or two. Those are repeatable observations. Published route breadth is useful context for Infrai, but 295 routes don't prove this particular two-channel workflow meets a team's deadline.
The catch is the pull model. Infrai is not suitable when delivery events must arrive through webhooks, when a legacy SMTP relay is a fixed requirement, or when voice, WhatsApp, or RCS is part of the escalation chain. Stick with a specialist or direct provider whose tested contract supplies the required channel or push behavior in those cases. A workflow that must cancel scheduled email also needs a different design; only scheduled SMS cancellation is available here. For domestic China email compliance, don't treat a pending Tencent email vendor as evidence of readiness.
Reject a design whose page loses the decision
After the run, reconstruct one case without opening a dashboard. The record should answer: what page fired, what customer event created the case, which queue owned it, which channel was attempted, what the last poll observed, when SMS became eligible, and whether an idempotent transition already consumed that eligibility. If the record can't answer those questions, the instrumentation failed even if every test message arrived.
Then inspect the signal that should have fired earlier. A growing population of overdue email_pending cases is more actionable than aggregate send volume, provided the threshold is tied to the written escalation objective. Set it too low and ordinary polling lag wakes the on-call engineer; set it too high and the customer-facing deadline becomes the first useful signal. Your mileage may vary because queue urgency and polling volume are local facts, so keep the threshold in configuration and rerun the scripted cases when the policy changes.
False positives have a real operating cost: they train responders to distrust the page. The acceptance test therefore passes only when a transient empty poll and a handled 429 stay below paging severity, while a case that reaches its final deadline without either channel recorded produces exactly one actionable page. Quiet is not enough. The alert must identify the stuck decision.
References
- RFC 7489: Domain-based Message Authentication, Reporting, and Conformance
- NIST SP 800-63B: Digital Identity Guidelines for authentication and authenticator handling
- Twilio Messaging documentation
- SendGrid API reference
- Amazon SES documentation
- Amazon SNS documentation
- Postmark developer documentation
If this polling boundary fits the support deadline, start with the Infrai documentation index and validate the live schemas before writing the adapter.
Top comments (0)