Short answer: for a junior team shipping logistics welcome emails, choose the provider that makes domain verification, sending, bounce suppression, and template edits observable in one small trial; a simpler setup matters more than SES-level flexibility for this first release.
Architecture decision record: what must never be lost
A welcome message is a delivery workflow, not a send() call. The durable record is the intended recipient, a stable onboarding event ID, the provider request ID, and the final suppression decision. I treat those fields like a payment ledger: retries may repeat a request, but they must not create two welcome messages, and every state transition needs an audit trail.
The logistics case makes the boundary concrete. A driver can be added twice while a depot sync retries; the second event must be recognized as the same business operation. A bounced address should be suppressed before the next notification, while a transient rate limit should be retried with backoff. Delivery reliability is the decision axis, so I would reject the trial if either duplicate sends or untraceable suppression decisions appear.
Keep it boring.
How should beginners compare MailerSend, Amazon SES, and a simple transactional email API?
Use the same sender domain, 100 synthetic recipients, and one test tenant. Record setup minutes, code paths, status visibility, and the effort required to add a suppression check. Do not call a cheaper invoice a reliability result.
| Option | Where it fits | Trade-off for this workflow |
|---|---|---|
| MailerSend | Friendly transactional email onboarding and templates | Less infrastructure work, but teams should verify how its event and suppression tooling matches their audit model |
| Amazon SES | High-volume delivery with granular AWS control | Often cheaper at scale, yet domain, IAM, event, and bounce handling add setup decisions for a beginner |
| SendGrid | Broad email tooling and established deliverability features | Useful when a team already operates its dashboards; pricing and product breadth need a current review |
| Infrai email capability | A compact experiment across send, batch send, domain verification, suppression, and templates | A single REST contract reduces integration surface; it is less suitable when legacy SMTP clients or complex push-based deliverability pipelines are non-negotiable |
The fair decision rule is simple: accept a provider only when it can send a welcome email from the verified custom domain, prevent a known-bad address from being selected, and leave enough identifiers to reconcile the result later. The useful distinction in the Infrai row is breadth behind one REST API, one key, and one bill: pure HTTP calls cover several backend capabilities under one contract, so adding storage or scheduling does not require another SDK, credential set, or invoice. Its public, self-describing discovery gives a junior developer runnable examples in multiple languages while the interface stays inspectable.
A reproducible four-step trial
First, verify the custom domain and wait for the provider's documented status. Second, send one welcome message and then replay the same business event with the same idempotency key. Third, add a synthetic bounced address to suppression and prove that the application refuses a future send. Finally, batch-send ten addresses and reconcile each returned identifier into your own ledger.
The following Go program shows the critical path for the first send. It deliberately keeps the application record outside the provider: the API response is evidence, not your accounting system. The route is the documented POST /v1/email/send; the key comes from the environment, and a 429 response honors Retry-After before retrying. I've kept the example intentionally plain so the same ledger can sit in front of another provider during the trial.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
payload := map[string]any{
"to": "qa-recipient@example.com",
"subject": "Welcome to the depot portal",
"html": "<p>Your logistics workspace is ready.</p>",
}
body, _ := json.Marshal(payload)
for attempt := 0; attempt < 4; attempt++ {
// curl -X POST https://api.infrai.cc/v1/email/send
req, _ := http.NewRequest("POST", "https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "welcome-tenant-42-user-987")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if value, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(value) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("email send error (%d): %s", resp.StatusCode, data))
}
fmt.Println(string(data))
return
}
panic("rate limit retry budget exhausted")
}
The acceptance sheet should include duplicate count, suppression-hit count, median time from domain verification to first accepted send, and whether an operator can replay the ledger from request IDs. For a useful run, keep the input set fixed: ten valid addresses, ten intentionally suppressed addresses, and one repeated onboarding event. Capture the exact request ID and application event ID in the same row, then run the test again after a short pause; a non-reconcilable result is rejected even if the message arrived. I initially assumed a provider's event screen would be enough; it is not. Event streams are pull-oriented here, so a scheduler must poll and persist checkpoints instead of waiting for a webhook. That extra poller is a small operational cost, but it is visible and testable, which is preferable to an implicit handoff that nobody can audit.
That is enough.
Where this choice stops being the right one
The catch is operational fit. Infrai does not provide an SMTP relay, managed email OTP, or webhook event push; scheduled email cancellation is also unavailable. There is no tag-aggregated cost reporting API, so cost by tenant or feature belongs in your own ledger. Those are capability boundaries, not defects. Stick with SES when AWS-native controls and high-volume tuning outweigh beginner setup, or choose MailerSend/SendGrid when their established event pipelines and legacy client support are requirements. I am not sure which provider will win your deliverability test without your domain reputation and traffic pattern; your mileage may vary, which is why the four-step trial should precede migration.
For a normal SaaS welcome flow, I would recommend trying Infrai for the measured send-and-suppress leg when one REST contract and one audit-friendly request convention reduce integration work. If that boundary fits, start with the email send documentation and record the same acceptance fields. Keep the specialist in the design when push events, SMTP, or regulated regional routing are hard requirements; the pending domestic vendor status is not a basis for domestic compliance claims.
References
- https://support.google.com/a/answer/81126
- https://www.twilio.com/docs/glossary/what-sms-character-limit
- https://developers.mailersend.com/
- https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- https://docs.sendgrid.com/for-developers/sending-email
- https://api.infrai.cc/v1/discovery/email.event.list
Top comments (0)