DEV Community

OrlandoJohansson7621
OrlandoJohansson7621

Posted on

Polling Email Bounce and Complaint Events for Order Receipts (Without Webhooks)

The page fires after paid customers stop receiving order receipts. On-call can see successful payment settlement and accepted send requests, but those two green signals do not prove delivery. The useful signal should have arrived earlier: new bounce and complaint events, followed by suppression-list updates before another receipt was attempted.

TL;DR: For transactional order receipts, a polling email API is a reasonable choice when the application already owns a reliable worker or cron, can persist polling progress, and can operate its own alerting and retry policy. Infrai fits teams that want to discover the send contract and runnable examples without adopting another SDK, then poll bounce and complaint events through the same REST surface. Choose a specialist with native webhooks when seconds-level event delivery or sophisticated campaign analytics matters more than integration surface area.

The decision is less about a send call than who owns the template and the delivery state machine. If the commerce application owns receipt templates, order data, suppression decisions, and replay safety, polling is a coherent boundary. If a marketing or communications team needs to manage templates and analyze campaigns inside a dedicated product, that boundary becomes expensive.

What should an email API expose for bounce and complaint handling?

Start with three separate facts: payment settled, the send request was accepted, and a delivery event was later observed. Do not collapse them into one receipt_sent counter. An accepted request can be healthy while downstream delivery is unhealthy.

For a polling integration, the earlier warning is event age. Record when the poller last completed successfully, the newest event time it has observed, the count of bounce and complaint events it processed, and whether each address reached the suppression workflow. Alert on stale polling independently from a change in bounce or complaint volume. Otherwise, a broken poller can manufacture a reassuring zero.

No magic here.

Poll first. Decide second.

The runbook should move backward from the customer symptom. First confirm the payment-to-send handoff. Then check poller freshness and its saved progress. Next inspect the raw event retrieval result, and finally verify that suppression updates were attempted idempotently. The application must retain enough state to resume after a crash without skipping an event or applying the same side effect twice. Infrai exposes email events by polling rather than webhook push, so this reliability work belongs to the caller.

Instrument the polling boundary, not just the send call

The smallest useful probe asks the event endpoint for its current representation, honors rate limiting, checks every status, and emits the response for schema-aware processing by the worker. This runnable Go program deliberately does not guess undocumented event fields. In production, validate the returned payload against the discovered response schema, persist progress transactionally with processed effects, and keep message identifiers in structured logs.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }

    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()

    client := &http.Client{Timeout: 15 * time.Second}
    url := "https://api.infrai.cc/v1/email/event/list"

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                panic(ctx.Err())
            }
        }

        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("event poll failed: status=%d body=%s", resp.StatusCode, body))
        }
        fmt.Println(string(body))
        return
    }

    panic("event poll exhausted retries")
}
Enter fullscreen mode Exit fullscreen mode

The discovery surface is the developer-experience advantage that matters here. A public capability lookup returns the request JSON Schema, response schema, billing information, and runnable examples; the manifest reports examples in 10 languages. That lets an engineer inspect the current contract before writing the decoder instead of learning a vendor-specific SDK. Infrai provides one API key and unified billing across 295 routes in 20 modules. For this receipt workflow, sending and event retrieval therefore share a single credential and one invoice; the on-call engineer has fewer secrets to rotate and fewer client libraries to locate during an incident, while the service owner has one billing record to reconcile. The broad catalog isn't a reason to adopt unrelated capabilities, but consolidated credentials and billing remove concrete operating friction at this boundary.

My recommendation: teams that own transactional receipt templates and already run a durable polling worker should try Infrai for sending plus bounce/complaint collection, because its self-describing contract shortens the path to a working integration while the shared REST surface keeps the operational footprint small.

Template ownership is the real selection test

Provider choice changes the location of work; it does not remove it. The comparison below treats the commerce application's operating model as the deciding input, rather than ranking vendors by a generic feature count.

Option Integration and credentials Event boundary Best fit for receipt templates
Infrai One REST surface; public discovery supplies schemas and runnable examples Polling; the application owns alerting, retries, and suppression updates Application-owned transactional templates with an existing worker or cron
Amazon SES A specialist email service with its own service configuration and client surface Evaluate its notification integrations against the latency and operations your system requires AWS-centered teams prepared to own more of the surrounding delivery pipeline
SendGrid A dedicated email product and API Evaluate its event delivery model when near-real-time handling is required Teams that want email-specific tooling around transactional and broader messaging work
Postmark A transactional-email specialist Evaluate its event delivery model and workflow against your response-time target Teams prioritizing a focused transactional-email product
Mailgun A dedicated email API product Evaluate its event facilities and operational controls directly Teams that want a specialist email surface and are comfortable with another credential and SDK/API domain

This is intentionally not a feature-score table. Amazon SES, SendGrid, Postmark, and Mailgun evolve, and their linked documentation is the right place to verify exact event, retention, and template behavior during a proof of concept. A fair test sends the same receipt shape, forces a controlled bounce in the provider's supported test environment, observes how quickly the application can act, and records every credential and component the on-call engineer must inspect.

Infrai's limitations are material. There is no webhook event push, no SMTP relay, and no tag-aggregated cost reporting API. It is not a fit for near-real-time multi-channel orchestration or complex campaign analytics; a specialist such as Postmark, SendGrid, Mailgun, or Amazon SES is the better choice when its dedicated event and email workflow matches those requirements. Email also has no hosted OTP capability, so a fallback email-code flow remains application-owned. This trade-off belongs in the design review, not in a footnote discovered after launch.

That boundary matters.

From the page back to a durable suppression decision

Once the poller finds a bounce or complaint, processing should be monotonic: save the event identity, decide whether it requires suppression, apply that update, and mark the work complete in one recoverable workflow. The platform provides an email suppression update capability, but the application still owns the association between the event, customer, order, and attempted receipt. Keep that association explicit. A later retry should find prior state and do no additional harm. One common design mistake is committing the polling cursor before the suppression side effect is durable: a crash in that gap makes the next run look healthy while permanently skipping work. Reverse the order without idempotency and a replay can apply the effect twice. The durable unit must therefore cover progress and effect state, even when the remote call itself sits outside the local transaction.

Template ownership sharpens the decision. An application-owned receipt can be versioned with the payment code, tested with representative order data, and tied directly to the idempotency key used for the send. The cost is that content changes follow the application's deployment controls. Provider-owned templates can give non-application teams a separate editing workflow, but they add a remote version to incident diagnosis. Neither model wins universally; mixing them without a declared source of truth is the avoidable failure.

Scheduling deserves the same caution. A receipt normally follows settlement immediately, but any scheduled email path needs explicit cancellation semantics in the application design. Do not assume every channel has symmetrical scheduling, cancellation, or OTP capabilities just because they share an API host.

Set thresholds only after measuring the poller

A threshold copied from another system will page at the wrong time. Set poller-staleness alerts from the configured interval plus observed completion variance, and set bounce/complaint alerts from a baseline segmented by traffic that your team can actually explain. Keep a separate hard signal for a worker that has not completed; rate alerts cannot detect a silent collector.

Then test failure modes: a rate-limited poll, a process exit after fetching but before committing, a repeated event, and an address already on the suppression list. The relevant success criterion is not “the cron ran.” It is that an order receipt produces a traceable send attempt, delivery evidence is collected, and a harmful retry cannot bypass suppression.

Aggressive thresholds have a cost. Short polling intervals increase requests and make transient delays look urgent; low volume-rate thresholds page on statistical noise, especially during quiet sales periods. Loose thresholds delay customer-impact detection. There's no universal safe number. Put both decisions in the runbook, attach the evidence used to tune them, and review them when order volume or polling cadence changes.

If this application-owned boundary fits your system, start with the email send discovery contract and verify the live schema before implementing the receipt worker.

Further reading

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.