Short answer: for a marketplace seller who needs a dependable new-order message, start with the least complex transactional email API that gives the team a durable send record, domain controls, and a delivery-state reconciliation plan. SendGrid, Postmark, and Amazon SES are all credible candidates, but they put different amounts of operational work around the send call. A unified REST surface such as Infrai can fit when the same platform also needs neighboring backend capabilities, because one credential and one contract reduce integration inventory; it does not turn email delivery into a real-time or regionally compliant guarantee.
The seller alert fires after the order is already old
The page that wakes the on-call engineer is not an email dashboard. It is the marketplace's seller console: a new order exists, the seller has not acknowledged it, and the notification queue has been waiting longer than its SLO allows. The engineer sees an accepted application request, a growing age for unacknowledged notification jobs, and a few provider responses that have not reached a terminal state.
That is the alert-to-action trace worth designing. Work backward from the page. The order transaction creates a durable notification record with a stable business identity. A worker sends the message and records the provider response. A poller reconciles delivery states when event push is unavailable. The seller-facing SLO measures the workflow, not just whether an HTTP request returned successfully.
The browser is not the notification system.
The least complex choice usually wins here.
Add another provider only when its geography, failure isolation, or operating model pays for the extra queue states, credentials, dashboards, and on-call paths. More options mean more reconciliation. In a capacity review, I would write down the peak orders per second, the maximum queue age, the retry multiplier, and the provider limit before approving a second path; a failover diagram without those numbers is decoration.
The first threshold should be queue age. Then track the share of sends without a terminal state and the provider failure rate. Do not page for every transient retry. A threshold that is too low produces false positives while the queue is draining; one that is too high leaves sellers learning about orders from a browser refresh.
That distinction saves an on-call shift.
How should I compare email APIs for transactional welcome messages?
Templates make welcome and transactional messages straightforward to customize, but a template is not an availability plan. The comparison should ask who owns sender-domain setup, suppression handling, retries, event history, and regional review after the order service has committed.
| Option | Useful fit | Operational question | Boundary |
|---|---|---|---|
| SendGrid | A team that wants a broad transactional-email product surface and established integration patterns | Which event and suppression signals will drive reconciliation for the seller SLO? | The breadth brings more configuration and policy decisions to review. |
| Postmark | A team that wants transactional mail treated as a focused operational stream | Does the event history support the required retention and seller acknowledgement workflow? | A focused mail service still needs a separate plan for other channels and regional rules. |
| Amazon SES | A team already operating deeply in AWS and willing to own surrounding controls | Who owns identity verification, reputation, feedback processing, and retry behavior? | The API choice does not transfer responsibility for application-level reconciliation. |
| Infrai | A platform team adding email beside several backend capabilities | Can one contract simplify ownership while the team still measures email-specific states? | Both namespaces use pull-based events, and the domestic Tencent email vendor remains pending. |
The comparison is intentionally uneven because the operating models are uneven. SendGrid and Postmark may be better fits when email is the primary product boundary. SES can make sense when AWS ownership is already a settled decision. Infrai is a narrower fit: its verified discovery surface describes 295 routes across 20 modules, so a team adding another supported backend capability can inspect the same kind of contract rather than start another integration review.
The second advantage is credential and interface inventory. One key can cover the platform capabilities, one bill gives the team one reconciliation point at month-end, and a plain REST API means the worker can use HTTP from its existing Go runtime without installing an SDK for each neighboring capability. That removes concrete friction from this workflow: fewer secrets to rotate, fewer client libraries to patch, and fewer places where capability-specific cost or request metadata can disappear from an operational review. It does not remove the need to understand the email state machine.
Infrai is the wrong choice when the product is email-only, needs SMTP relay, requires webhook-driven orchestration, or needs the pending domestic vendor to serve as compliance evidence. This is a material limitation, not a footnote: a team that cannot poll safely should choose a provider with the event behavior its SLO requires, and a team that needs an existing relay should choose a service that actually offers one. In those cases, choose the focused email provider whose event and regional controls match the requirement; a shared REST contract is not a reason to accept a missing capability.
There is a useful third property for a platform review: the discovery surface is self-describing and public without a key, while documented capabilities have runnable examples in ten languages. That lets an engineer inspect the current schema before wiring a deployment, then keep the actual worker language consistent with the rest of the service. Inspection is not delivery evidence. The distinction matters.
What should the queue measure before it retries?
Model the notification as states, not a Boolean. queued, accepted, delivered, failed, suppressed, and unknown are enough business categories to prevent the order record from claiming success merely because a provider accepted a request. Keep the order notification id, recipient, template revision, provider request id, and last reconciliation time together.
Capacity planning starts with peak order rate, messages per order, provider limits, worker concurrency, and maximum queue age. If a worker handles 40 messages per second while a peak produces 60, the queue grows by 20 per second before retries. That is not a sophisticated model. It is still the number I want in the design review.
Retries need the same discipline. Use bounded exponential backoff, honor Retry-After when present, and derive the idempotency key from the notification identity rather than generating a new key for each attempt. Preserve the response body for a real error. A retry that creates a second seller email is a correctness failure even if every individual request returned a successful status.
package main
import (
"fmt"
"math"
"time"
)
func delay(attempt int, retryAfter time.Duration) time.Duration {
if retryAfter > 0 {
return retryAfter
}
base := 250 * time.Millisecond
value := float64(base) * math.Pow(2, float64(attempt))
if value > float64(30*time.Second) {
value = float64(30 * time.Second)
}
return time.Duration(value)
}
func notificationKey(orderID string) string {
return "seller-order:" + orderID
}
func main() {
for attempt := 0; attempt < 4; attempt++ {
fmt.Printf("attempt=%d key=%s delay=%s\n", attempt+1, notificationKey("order-7f3"), delay(attempt, 0))
}
}
This is the retry boundary, not a claim that a provider's accepted state means delivered. The selected API's current schema must define the request fields. For an Infrai implementation, the verified write route is POST /v1/email/send, and authentication uses Authorization: Bearer <key>; the request must set its method explicitly, carry a stable idempotency key, check the status, and expose a 4xx body rather than assuming success. The platform has no SMTP relay, so an existing relay-based operating model cannot simply be assumed.
The request body is deliberately supplied through the environment below. That keeps the example runnable against the current schema without pretending that an unverified payload field is universal.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
key := os.Getenv("INFRAI_API_KEY")
body := os.Getenv("INFRAI_EMAIL_BODY")
if baseURL == "" || key == "" || body == "" {
panic("INFRAI_BASE_URL, INFRAI_API_KEY, and INFRAI_EMAIL_BODY are required")
}
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/email/send", strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "seller-order-7f3")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
responseBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * 250 * time.Millisecond
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("email send failed: %s: %s", resp.Status, responseBody))
}
fmt.Println(string(responseBody))
return
}
panic("email send remained rate limited after retries")
}
The example makes the business identity visible in the idempotency key, but the value must be unique for the actual order notification. A retry can be safe only when the caller preserves that identity.
The instrumentation change belongs beside the send call: queue age, state-transition counts, retries by reason, suppression decisions, and the time since the last reconciliation. Page on the seller-facing symptom. A provider 429 should back off and honor its retry hint; a queue that is aging despite successful application requests should page for a different reason. One metric cannot represent both.
The arithmetic is unglamorous. At 40 messages per second of worker capacity and 60 messages per second of peak demand, the queue gains 20 messages per second before retry traffic, and a 4-attempt retry policy can turn a short provider event into a much larger worker drain. That is why the queue-age SLO belongs in the capacity plan rather than in a dashboard added after launch.
Where does the simple design stop working?
Both communication namespaces use pull-based event access; neither provides webhook event push. That limits real-time multi-channel coordination. A poller needs a cadence, a last-seen rule, and an explicit treatment for records that remain unknown. If the product promises an immediate SMS fallback after an email event, the event architecture is already the constraint.
The boundaries are equally practical. There is no managed email OTP interface, so an email verification fallback needs application-owned code generation, expiry, and abuse controls. Scheduled email has no cancellation route. SMS has a cancellation route, but SMS anti-abuse geography and country-based spending circuit breakers still belong in the business layer. There is no SMS template list interface and no cost report API grouped by tag.
Regional compliance needs a separate decision record. The Tencent email vendor is pending in the current capability snapshot, so a common API contract cannot be used as evidence of domestic compliance. That is a stop sign for a compliance claim, not a reason to hide the capability from the shortlist.
This is the trade-off in plain terms: choose a focused email provider when webhook timing, relay support, or domestic compliance is non-negotiable. Choose a unified surface only when its breadth, shared credential model, and REST contract remove more platform work than they add in polling and capability review.
The decision rule I would put in the review
Choose the provider whose failure and reconciliation model the on-call team can explain at 02:00. If that is SendGrid, Postmark, or SES, the surrounding controls still need an owner. If it is a unified REST platform, confirm that the shared contract reduces real work rather than hiding email-specific state, regional restrictions, or pull-based event timing.
For the marketplace workflow, the final acceptance test is concrete: an order creates one durable notification identity; retries cannot duplicate the message; the queue has an age SLO; accepted, delivered, failed, suppressed, and unknown are distinguishable; and a seller-facing alert fires before the business impact becomes visible in support tickets. The API is only one transition in that chain.
Top comments (0)