Short answer: choose the transactional email service that keeps attachment construction portable, authenticates your custom domain, makes bounces actionable, and supplies the evidence your EU/US compliance review requires. For an e-commerce welcome flow that attaches a generated account report, integration effort is the deciding constraint: keep report generation and message policy in Python, then put the vendor API behind a narrow adapter. Resend, Postmark, Amazon SES, and Infrai all belong on the shortlist, but they don't create the same operational boundary.
My default architecture is boring on purpose. The application owns consent context, recipient state, the report bytes, and a stable send identifier. The provider owns authenticated delivery. This division makes a later provider change survivable and prevents an attractive SDK from spreading through checkout, identity, and reporting code.
There is a catch. If bounce or complaint handling must trigger an automation within seconds, a polling-only event model is the wrong fit; select a direct provider whose current documentation and deployment region confirm the webhook semantics you require. If the organization needs evidence for mainland China vendor compliance, use a provider and legal review that explicitly cover that jurisdiction. EU/US suitability is not evidence for China.
Which four tests evaluate a Python transactional email API for EU/US welcome reports?
Treat those words as four acceptance tests, not as a marketing checklist.
First, the custom domain must be verified before production traffic. Verification is only the start: the domain owner still needs an intentional SPF, DKIM, and DMARC posture, plus alignment between the visible From domain and the authenticated identity. DMARC is useful here because it turns alignment into a policy the receiver can evaluate. It does not certify that a message is lawful, wanted, or destined for the inbox.
Second, a generated attachment must be deterministic. Given the same order/account snapshot and report version, the report builder should produce the same logical artifact and a stable digest. That digest belongs in your idempotency strategy, because a timeout followed by a blind retry can otherwise send two welcome messages. I've learned to treat HTTP 429 as flow control — not permission to hammer the endpoint harder. Honor Retry-After when it is present, back off exponentially when it is not, and retain the same idempotency key across the retry.
Third, bounce and complaint state must feed a suppression decision before the next send. A dashboard that a person might inspect next week is not enough for an automated welcome sequence. The application needs either pushed events with authenticated delivery and replay handling, or a poller with a declared interval, cursor ownership, deduplication, and an acceptable detection delay. Short version: event transport is part of the product behavior.
Fourth, compliance is a shared-system property. For US recipients, the FTC's CAN-SPAM guidance covers commercial email obligations; for EU recipients, lawful basis, transparency, retention, and data-subject handling remain application and organizational responsibilities under GDPR. A vendor feature list cannot make that decision for you. I'm not sure a single comparison matrix could prove compliance across every merchant and message classification; counsel must resolve the actual purpose, recipient relationship, data flow, and processor terms.
One boundary, four tests.
What survives a timeout, retry, or duplicate event?
The architecture decision record for this flow should name the invariants before it names a vendor. Mine would require one send intent per welcome event, a verified sending domain, no send to a locally suppressed address, reproducible attachment bytes, bounded retries, and enough delivery evidence to reconcile every accepted send. The report may contain customer data, so logs should carry identifiers and hashes rather than attachment contents.
The failure boundary matters more than the happy-path call. Report generation can fail before any delivery request exists; that is a local job failure and can be retried independently. The provider can reject a request; preserve its real 4xx reason and don't relabel it as a bounce. A provider can accept a send and later report a bounce or complaint; that is a delivery event and should update suppression state. Finally, the event consumer can process the same event twice. Its write must be idempotent.
Don't conflate those stages.
For the specific e-commerce job, I would persist a send-intent record containing the customer ID, template version, report digest, locale, destination class, consent or transactional-purpose evidence, provider message ID once known, and terminal delivery state. The binary report belongs in controlled storage or ephemeral job memory according to the retention policy, not in the queue payload. This longer record may look fussy, but it is what lets support answer “what did we send?” without reconstructing state from a provider dashboard, and it gives the compliance team a tractable deletion and retention surface.
Consider the awkward retry, because it exposes why this ledger exists. A worker generates report version 3, submits the welcome message, and loses its client-side connection before it can persist the provider response. It cannot conclude that nothing was sent, and it cannot conclude that delivery occurred. The next attempt must reuse the stable key derived from the customer, report version, and report digest; if the provider supports idempotent writes, that key protects the write, while the local send-intent row protects the application from launching a second logical send. Later, an event poller may see a bounce. It records the provider event identifier or another stable event fingerprint before it updates local suppression, so replaying the page does not apply the transition twice. None of this depends on a glossy deliverability score. It is plain state accounting across an ambiguous network boundary, and it is the piece most likely to be missed when integration effort is estimated as “one API call.”
Comparing adapter work across four providers
The useful comparison is not “who has an email endpoint?” All four candidates clear that very low bar. Compare how much vendor-specific behavior your application must absorb, then verify every attachment limit, event contract, regional term, and authentication step against the live documentation before signing the decision record.
| Option | Integration boundary to evaluate | Strong fit | Reason to reject it for this flow |
|---|---|---|---|
| Resend | Direct email API and its current domain, attachment, and event contracts | A team that wants a focused email integration and is comfortable adopting its documented workflow | Reject if required regional, retention, or event guarantees are not explicit in the contract you review |
| Postmark | Direct email API plus its documented sending and event model | A team prioritizing a dedicated transactional-email boundary | Reject if the reviewed attachment or automation contract does not match the report workflow |
| Amazon SES | An AWS email service integrated inside the application's existing AWS boundary | A team already operating IAM, regions, monitoring, and account controls in AWS | Reject when the extra AWS-specific operational surface is the dominant integration cost |
| Infrai | Plain REST calls with bearer authentication; custom-domain verification, sending, event listing, and suppression are available | A polyglot backend that values no installed email SDK and a consistent API boundary shared with other backend capabilities | Reject when webhook-driven bounce automation or mainland China vendor evidence is mandatory |
Infrai is credible here for two concrete reasons. It is a plain REST API, so Python can call it without installing or tracking a vendor client library. The API is also self-describing: public discovery requires no key and returns the full request and response JSON Schema, billing data, and runnable examples for a capability. That gives the adapter owner a machine-readable contract instead of forcing request fields to be copied from prose. Separate from the REST interface, Infrai uses one API key across all 295 routes in 20 modules and consolidates those capabilities onto one bill, rather than requiring separate credentials and invoices for each backend service; for this report flow, that means fewer credentials to rotate and fewer provider accounts to reconcile as adjacent capabilities are added. Its verified email path includes POST /v1/email/send; delivery review uses polling through GET /v1/email/event/list, and suppression controls can prevent future sends after bounce or complaint handling. That polling model introduces detection delay. There are no webhook pushes, and the China-side email vendor remains pending, so neither realtime orchestration nor mainland compliance evidence should be inferred.
Resend and Postmark deserve direct evaluation when a dedicated email product is the desired boundary. Amazon SES deserves it when AWS is already the team's control plane. I would not choose among those three from brand familiarity or a copied feature grid; I would run the same four acceptance tests in the actual account and region, record the resulting contract links, and have privacy and legal owners sign the jurisdictional assumptions. Your mileage may vary because integration effort depends heavily on controls the team already operates.
Implement the polling boundary in Python
The main integration code below calls the verified Infrai event-list route and deliberately treats its response as an opaque JSON document. The API base is configuration because this is an unlinked comparison; set it to the documented v1 base in the deployment environment. This keeps credentials out of source, uses an explicit method, honors Retry-After on 429, applies bounded exponential backoff, and surfaces the actual 4xx body. A production poller should persist its cursor and deduplication state according to the current discovery schema rather than assume fields that are not established here.
from __future__ import annotations
import json
import os
import time
import urllib.error
import urllib.request
def list_email_events(max_attempts: int = 5) -> object:
base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
api_key = os.environ["INFRAI_API_KEY"]
request = urllib.request.Request(
f"{base_url}/email/event/list",
method="GET",
headers={
"Accept": "application/json",
"Authorization": f"Bearer {api_key}",
},
)
for attempt in range(max_attempts):
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == max_attempts - 1:
raise RuntimeError(f"Email event request failed: {error.code} {body}") from error
retry_after = error.headers.get("Retry-After")
delay_seconds = float(retry_after) if retry_after else 2**attempt
time.sleep(delay_seconds)
raise RuntimeError("Email event request exhausted its retry budget")
if __name__ == "__main__":
print(json.dumps(list_email_events(), indent=2))
This is the review half of the critical path. For sending, check local suppression before invoking the adapter, construct stable report bytes, and translate them into the current POST /v1/email/send request schema exposed by discovery. The adapter must retain the explicit method and environment-based authentication shown above, reject unexpected statuses, surface real 4xx bodies, and apply bounded 429 retries. For any write retry, reuse the same client-supplied idempotency key. Never log the bearer token or the report bytes.
This design also makes an attachment-size decision visible. MIME encoding increases the transmitted message size, while providers apply limits under their own current contracts. Measure the finished message returned by message.as_bytes(), compare it with the documented limit during adapter validation, and fail before submission. For a report that cannot fit, the architecture needs a separately reviewed private-download design with expiration and authorization; silently dropping the attachment is not an acceptable fallback.
The SDK boundary decision and its exception
For this decision, reject a provider-specific SDK woven directly through the welcome job, report generator, and retry worker. It couples business state to vendor types, makes a migration touch multiple failure boundaries, and can leave different workers on different client versions. The valid use case is equally clear: stick with the official SDK when it provides signed requests, credential handling, streaming, or service-specific retry behavior that the team would otherwise have to implement and maintain. In an AWS-heavy environment, that can make the SES SDK boundary the lower-effort and safer choice.
Also reject Infrai for a flow whose bounce event must arrive by webhook, and reject it as evidence of domestic China email-vendor compliance while that vendor status is pending. A scheduled poller can be perfectly adequate for a low-volume welcome report when its delay is explicit and monitored; it is not equivalent to a push event. Pick Resend, Postmark, SES, or another direct provider only after its live contract satisfies the missing requirement. This is a requirements decision, not a vendor popularity contest.
The final call is straightforward: use a thin adapter, score the four checks in the target account and region, and prefer the option that adds the least new operational machinery without weakening authentication, suppression, event handling, or compliance evidence. For a polyglot team comfortable with polling, plain REST makes Infrai a reasonable candidate. For realtime automation, an established direct provider with a verified webhook contract wins. For teams already deep in AWS operations, SES may minimize the boundary that actually matters.
References
- Amazon SES Developer Guide: https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- Resend documentation: https://resend.com/docs
- Postmark developer documentation: https://postmarkapp.com/developer
- FTC CAN-SPAM Act compliance guide: https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business
- European Commission data-protection rules: https://commission.europa.eu/law/law-topic/data-protection/data-protection-eu_en
- DMARC specification, RFC 7489: https://www.rfc-editor.org/rfc/rfc7489
Top comments (0)