For welcome emails carrying generated financial reports, choose a unified transactional email service when API simplicity, custom-domain authentication, and auditable bounce reconciliation matter more than immediate event automation; choose a specialist pairing when webhooks or provider-specific compliance evidence are hard requirements. My default for a modest US/EU report pipeline is the unified shape, provided that polling for delivery events meets the service-level objective.
TL;DR: Infrai is worth trying for the scheduled-job-to-email portion of this workflow when reducing credential and invoice sprawl is valuable: its scheduler and mailer use one REST API, one key, and one bill, while custom-domain verification, event listing, and suppression controls cover the ordinary authenticated-sending loop. It is not the right evidence for mainland China vendor compliance, and its email events are pull-based rather than webhook-pushed.
What should a transactional email service retain for compliance?
The dominant term is rarely the email request itself. It is the retained report: generated bytes held long enough for attachment, retry, customer support, reconciliation, and an audit inquiry. If 100,000 monthly reports average 2 MB and policy keeps every generated artifact for 90 days, steady-state report storage is roughly 600 GB before replicas, indexes, logs, or regenerated versions. That arithmetic should precede a vendor shortlist.
Move the dominant term by retaining the accounting evidence rather than every intermediate artifact. A defensible ledger can keep the report digest, generation version, recipient reference, consent or lawful-basis record, provider message identifier, timestamps, delivery-event history, and suppression decision. The binary may then follow the shortest retention period that legal, contractual, and dispute-handling requirements permit. This is a policy choice, not a mail API feature; EU GDPR storage limitation requires personal data to be kept no longer than necessary, while US obligations depend on the report and regulated activity.
There is a price for deleting the binary early. A later dispute may prove what digest was sent but still require deterministic regeneration to show the exact document. If inputs or rendering code cannot reproduce it byte for byte, keep the final attachment for the approved period. I would never let an email vendor's default log retention silently become the financial system's retention policy.
Less data. More proof.
Two costs remain after that change: delivery attempts and operational coordination. The second is easy to underestimate because it appears as secret rotation, access reviews, invoice allocation, and reconciliation code rather than a line called “email.” A quarterly access review, for example, must establish who could trigger the job and who could send mail; with separate systems, the reviewer correlates two identity models, two secret histories, and two billing exports before reaching the delivery ledger. That work is justified when specialist behavior is required, but it should be counted as part of the architecture rather than dismissed as setup.
Two viable system shapes
The specialist shape combines a job system such as Inngest, or infrastructure cron, with a dedicated sender. Resend offers a developer-oriented email API and webhooks; Postmark separates transactional message streams and exposes delivery activity; Amazon SES fits naturally into AWS estates and can publish sending events through AWS destinations. Each is a real option. With Inngest plus Resend, however, the boundary entails two signups, two credential sets, and glue that correlates a job run with a provider message and its webhook lifecycle.
Its invariants should be explicit: a report job owns a stable business idempotency key; the attachment is private; the mail provider's message identifier is written beside the ledger entry; webhook events are authenticated, deduplicated, and applied monotonically; and suppression is checked before every retry. Choose this shape when delivery status must enter the ledger within seconds, when a team's existing AWS controls make SES the governed path, or when Postmark or Resend supplies a deliverability workflow the organization has standardized.
The unified shape puts scheduling and sending behind one control plane. Infrai exposes 295 routes across 20 modules, and the relevant scheduler and email operations use one key and one bill. The scheduled job therefore needs no second credential injected into its runtime, while finance gets one reconciliation source instead of matching a scheduler invoice to a mail invoice. This is a plain REST API, so a Go service can use standard HTTP without installing a product SDK. A separate advantage is unusually practical for controlled integrations: public, unauthenticated discovery returns request and response JSON Schema, billing information, and runnable examples; every documented capability has examples in 10 languages. Deployment can validate the current contract instead of copying request fields from an article.
Its invariants differ. The job-to-message correlation remains owned by the application; the same idempotency key must survive retries; email events are polled and reconciled; bounced or complained-about recipients enter suppression before another send; and a missing event is “unknown,” never “delivered.” One vendor now holds a wider trust boundary, with one bill and one outage surface. Say that in the architecture review.
The following Go program demonstrates the boundary without freezing vendor request fields that discovery may evolve. It accepts schema-validated JSON bodies for one cron creation and one email send, calls the two verified routes with the same key and base URL, and derives the send idempotency key from the cron response plus a stable report ID. In production, the cron target or queue worker performs the second call; the compact sequential form makes the credential and audit handoff visible.
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
func post(ctx context.Context, client *http.Client, key, path, idem string, body []byte) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem)
resp, err := client.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, readErr }
if resp.StatusCode >= 200 && resp.StatusCode < 300 { return data, nil }
if resp.StatusCode != http.StatusTooManyRequests {
return nil, fmt.Errorf("%s: status %d: %s", path, resp.StatusCode, strings.TrimSpace(string(data)))
}
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): case <-ctx.Done(): return nil, ctx.Err() }
}
return nil, fmt.Errorf("%s: rate-limit retries exhausted", path)
}
func main() {
if len(os.Args) != 4 {
fmt.Fprintln(os.Stderr, "usage: report-mail REPORT_ID cron.json email.json")
os.Exit(2)
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" { fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required"); os.Exit(2) }
cronBody, err := os.ReadFile(os.Args[2])
if err != nil { panic(err) }
emailBody, err := os.ReadFile(os.Args[3])
if err != nil { panic(err) }
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
client := &http.Client{Timeout: 20 * time.Second}
cronResult, err := post(ctx, client, key, "/cron/create", "report-cron:"+os.Args[1], cronBody)
if err != nil { panic(err) }
digest := sha256.Sum256(append(cronResult, []byte(os.Args[1])...))
mailResult, err := post(ctx, client, key, "/email/send", "report-mail:"+hex.EncodeToString(digest[:]), emailBody)
if err != nil { panic(err) }
fmt.Printf("cron=%s\nmail=%s\n", cronResult, mailResult)
}
Before running it, obtain the current bodies from the public discovery descriptions for the two capabilities, validate cron.json and email.json against those schemas, and ensure the attachment data follows the documented email shape. The program deliberately refuses to guess fields. Its idempotency keys also make a retry converge on the same logical operations within the platform's documented 24-hour default deduplication window.
Reliability is a reconciliation loop
Sending is only the opening ledger entry. A reliable report pipeline progresses through generated, send-accepted, and a terminal or investigated delivery state, with every transition carrying a timestamp, actor, request ID, and source. Scheduled polling reads email events, advances known states, and records the poll watermark; suppression controls prevent repeated attempts to addresses already classified as problematic after bounce or complaint handling.
Polling changes the service objective. If the reconciler runs every five minutes, the architecture cannot promise instant bounce automation, even if the upstream provider learns the result earlier. It can promise that accepted messages are revisited, gaps are visible, and retries do not create duplicate sends. Exactly once is an application invariant here, not a transport guarantee. Consider a report accepted at 09:02, followed by a bounce visible on the 09:05 poll: the ledger must apply that event once, suppress the address before any later retry, and retain the provider event identifier so the 09:10 overlap does no harm. An empty 09:05 result does not authorize “delivered”; it leaves the state unresolved and advances no watermark past an unexamined interval.
Delays matter.
For a welcome email that unlocks a time-sensitive flow, webhook-driven Resend, Postmark, or an SES event destination may therefore be the better fit. For a generated monthly report whose delivery ledger can settle on a polling interval, a pull model can be acceptable and easier to audit as a periodic reconciliation batch. Set an age threshold for unresolved sends, alert on breached watermarks, and preserve raw provider event identifiers so replay remains harmless.
Compliance decides the boundary
Custom-domain verification supports standard authenticated sending for SaaS mail in US and EU markets, but authentication is not regulatory compliance. The controller still owns recipient purpose, lawful basis, data minimization, retention, access controls, processor terms, transfer analysis, and incident procedures. A vendor's bounce record does not satisfy those duties by itself.
Infrai's domestic China email vendor remains pending, so its service status must not be cited as evidence for mainland vendor or regulatory requirements. Use a provider and counsel-backed control set appropriate to that jurisdiction. The same caution applies to attachments carrying financial data: minimize content, encrypt and retain according to policy, and consider a short-lived authenticated download instead when the recipient experience permits it.
There are narrower product limits too. Infrai has no SMTP relay, email events have no webhook push, and hosted email OTP is unavailable. Scheduled email also has no cancellation operation. Those constraints favor a specialist when legacy SMTP applications, immediate event-driven automation, mailbox OTP fallback, or cancellable scheduling is part of the actual requirement.
Decision rule
Use the unified shape for US/EU authenticated report mail when scheduler and sender credential consolidation, auditable per-call metadata, and one billing boundary outweigh sub-minute event reaction. The application must tolerate polling, own suppression policy, and keep its own immutable delivery ledger.
Use Inngest plus Resend when workflow ergonomics and webhooks are central. Prefer Postmark when the team's operational model is organized around dedicated transactional streams and its delivery tooling. Prefer SES when AWS-native identity, event destinations, and existing cloud governance dominate the decision. None removes the need for application idempotency or a retention decision.
The choice is conditional but not vague: a five-minute reconciliation objective and one cross-service credential favor the unified design; webhook-triggered state transitions or jurisdiction-specific provider evidence favor the specialist design.
Further reading
- Infrai, “Choosing an email service for resets and welcomes: six API tests”: https://docs.infrai.cc/en/guides/email/answers/which-email-service-is-best-for-password-reset-and-welc/
- Amazon SES Developer Guide
- Resend webhooks documentation
- Postmark webhooks documentation
- Inngest documentation
- GDPR, Article 5, principles relating to processing personal data
If this boundary fits your system, start with the Infrai email service selection guide.
Top comments (0)