Short answer: a simple SaaS deliverability dashboard can poll by message ID and show sent, delivered, or bounced verification email status, but it will be near-real-time rather than instant; keep the template in your application when portability matters, and store the provider receipt as an operational fact.
For an edtech signup flow, the important trade-off isn't charting. It is ownership. If the verification template, its version, and the account-signup intent live in the application database, a delivery provider remains an execution dependency rather than the source of truth. I would set a freshness objective before writing the worker, because a pull-only event surface makes “instant” an impossible promise even when every component is healthy.
Two minutes may be acceptable for an internal support panel. Two minutes may be unacceptable for an automated login decision. Those are different systems.
Model the receipt ledger before the charts
Consider a bounded failure scenario rather than an invented war story: a learner submits signup, the application renders verification template version 17, the send call returns a message ID, and the UI says “check your inbox.” Thirty seconds later, support needs to distinguish four conditions: the application never obtained a receipt, the provider accepted the message, delivery was recorded, or a failure event was recorded. Calling all four “email sent” destroys the one distinction the dashboard exists to provide.
The invariant is small: the application owns signup_id, recipient identity, template version, and the mapping to an opaque provider message ID. Provider state is observed data. Don't use a dashboard poll to decide whether the learner's token is valid, and don't silently turn a missing event into a bounce. Absence means unknown until the observation window or your explicit expiry policy closes.
I use five local states: receipt_missing, sent, delivered, bounced, and stale. The fifth is deliberately local. It says the polling SLO was missed; it does not pretend the provider reported a delivery outcome. This separation also gives the on-call engineer an actionable signal when the dashboard is old without mislabeling a learner's mailbox.
How should a simple SaaS dashboard poll transactional email events by message ID?
Persist the message ID as part of the send transaction, then let a scheduled worker fetch per-message detail or event lists and reconcile observations into the local ledger. The dashboard reads that ledger, never fans out to the delivery API during an admin page request. That keeps page latency independent of the provider and gives you one place to enforce backoff, concurrency limits, and a freshness SLO.
The first worker probe should preserve the response as evidence rather than guess at undocumented fields. This minimal Go program polls one stored message ID through the verified message-detail route, explicitly uses GET, authenticates from an environment variable, honors Retry-After on 429, and prints the successful response for the adapter contract test. Set INFRAI_API_ORIGIN to the API origin and keep the key out of source control.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if at, err := http.ParseTime(header); err == nil && time.Until(at) > 0 {
return time.Until(at)
}
return time.Second * time.Duration(1<<attempt)
}
func pollMessage(ctx context.Context, client *http.Client, origin, key, id string) ([]byte, error) {
route := strings.Replace("/v1/email/get/{id}", "{id}", url.PathEscape(id), 1)
endpoint := strings.TrimRight(origin, "/") + route
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("request returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("rate limit retry budget exhausted")
}
func main() {
origin := os.Getenv("INFRAI_API_ORIGIN")
key := os.Getenv("INFRAI_API_KEY")
messageID := os.Getenv("EMAIL_MESSAGE_ID")
if origin == "" || key == "" || messageID == "" {
fmt.Fprintln(os.Stderr, "set INFRAI_API_ORIGIN, INFRAI_API_KEY, and EMAIL_MESSAGE_ID")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := pollMessage(ctx, &http.Client{Timeout: 10 * time.Second}, origin, key, messageID)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
After inspecting that response against current discovery, map only documented fields into a local observation. The production Record operation should be an upsert keyed by message ID plus the observed event identity available from the adapter. Re-reading an event must not duplicate a transition. Reads don't create duplicate mail, but disciplined retry behavior still protects the shared quota.
Start the capacity model with arithmetic, not optimism. At 60,000 active verification messages and a five-minute target, one complete sweep needs 200 observations per second before retries. If the allowed request rate is lower, spread polling by age: check new receipts frequently, then lengthen the interval after each unchanged observation. I'm not sure what interval fits your support promise until the provider quota and peak signup rate are known; those two numbers settle it.
Short queues help.
Put template ownership under deployment governance
The send provider may render a hosted template, or your application may render content and submit the result. For signup verification, I favor application ownership when the platform team expects vendor changes, needs template versions tied to deployments, or must reproduce exactly what a learner received. The catch is real: your team then owns escaping, preview tests, localization, and the release path. Hosted templates are a better choice when non-engineering editors must publish frequently and accepting a provider-specific template lifecycle is an explicit trade.
Do not confuse transport portability with deliverability. A stable interface can reduce code change during a provider swap, but sender authentication and recipient behavior still govern inbox outcomes; Google's sender guidelines remain relevant regardless of the API wrapper. Template ownership gives you a clean control boundary. It does not grant an inbox-placement SLO.
Here is the buy-versus-build decision I would put in the design review:
| Option | Template owner | Operational attraction | The catch | Prefer it when |
|---|---|---|---|---|
| Infrai | Application or platform-managed template | One REST API over plain HTTP needs no vendor SDK, while a consistent interface lets the application switch vendors without changing its contract | Events are pull-only, there is no SMTP relay, and tag-aggregated cost reporting must be built locally | API portability and a scheduled freshness target matter more than webhooks |
| SendGrid | Decide during evaluation | Staying put avoids migration risk for an existing deployment | Revalidate the template workflow and event contract against current documentation | The team already operates it and a migration has no measured reliability benefit |
| Postmark | Decide during evaluation | A focused incumbent can reduce organizational change | Revalidate the same ownership and polling assumptions rather than assuming contract parity | Existing operations and templates already meet the signup SLO |
| Amazon SES | Application decision | Fits teams prepared to own more of the integration boundary | More ownership also means more platform work; verify the current event path before designing the ledger | The team intentionally accepts that engineering load |
| Mailgun | Decide during evaluation | Another credible transport option for a proof of concept | Treat its message IDs and event semantics as adapter-specific | A bake-off shows a better fit for the team's existing controls |
That table does not manufacture a winner from feature-counting. SendGrid, Postmark, Amazon SES, and Mailgun deserve a current documentation review and a small bake-off before a production choice; the available evidence here does not establish feature parity. Your mileage may vary, especially if the incumbent already has authenticated domains, mature runbooks, and trained support staff.
Compare transport contracts without manufacturing a winner
The admin view needs a timestamped state trail, template version, message ID, last poll time, and next eligible poll time. “Delivered” should mean a delivery event was observed. “Bounced” should mean a failure event was observed. “Sent” should not be painted green as if it proved receipt. Keep unknown and stale visually distinct because support decisions differ: unknown calls for patience or another poll, while stale calls for an operational check of the worker.
For the SLO, measure the age of the newest successful reconciliation and the share of pending messages observed within the target window. Count worker attempts and 429 responses, but avoid treating request volume as a success metric. A busy poller can be completely useless.
Campaign and budget rollups belong in your database too. Infrai has no tag-aggregated cost reporting API, so store the dimensions you need at send time and aggregate them locally; do not try to recover product, cohort, or campaign meaning from a provider message ID later. This is one reason a local receipt ledger pays for itself even when the visible dashboard starts as three status counts.
The hard boundary for pull-only event workflows
Reject it when a downstream workflow requires immediate event push, because the email and SMS event namespaces described here are pull-only. Stick with a provider and architecture whose verified webhook contract meets that deadline. Also reject an Infrai-based email design when SMTP relay, managed email OTP, voice, WhatsApp, or RCS is mandatory; those are capability boundaries, not details to hide in an adapter. A scheduled email also has no cancellation route, so don't schedule one if product requirements include a reliable cancel action.
For domestic China compliance, pending email vendor readiness is not evidence. Get the required legal and vendor review instead.
Polling is enough for beginner operational visibility when the panel is internal, event lag is acceptable, and the application can own a small ledger plus worker. It is not suitable for a real-time orchestration bus. That boundary is the decision.
References
- Google, Email sender guidelines: https://support.google.com/a/answer/81126
- Twilio, SMS character limits and segmentation: https://www.twilio.com/docs/glossary/what-sms-character-limit
Top comments (0)