TL;DR: For a budget-minded marketplace, use a transactional email service that can verify a dedicated sending domain, maintain suppressions, and expose bounce and complaint events. A poll-based API is practical when a small worker can own the delay and preserve raw evidence. Choose a webhook-first product instead when downstream actions must happen immediately, and choose SMTP support when migration compatibility is non-negotiable.
The concrete job here is narrow: notify a logistics marketplace seller that order ord_20260926_1842 is ready to fulfill. The important output is not merely an accepted send request. It is a trace from the order and message identifier to later delivery evidence, plus a deterministic decision about whether that seller address remains eligible for the next notification. For this workload, compliance evidence is the primary decision axis.
What should a practical startup transactional email deliverability stack prove?
Start with a dedicated domain and complete its verification and DKIM setup before production traffic. Keep the transactional stream apart from promotional mail. Then define four records that the application owns: the business event, the send attempt, the provider event, and the recipient's current suppression state. A provider dashboard can help an operator, but it is a poor system of record for an automated marketplace workflow.
For each order notification, generate one stable correlation ID and retain it across retries. Store the order ID, seller account ID, message ID returned by the provider, template revision, recipient-domain hash, accepted time, and the later event payload. Do not put an email address or order contents into logs just because the provider returned them. In the EU, that restraint supports data minimization; in the US, it also makes access control and retention reviews less painful. It does not, by itself, establish legal compliance.
The status model can stay small: pending, accepted, delivered, soft_failed, hard_failed, and complained. Only a hard failure or complaint should automatically place an address on the local suppression path; a soft failure needs a bounded retry policy. Those are application decisions, so document them beside the code and test them with recorded fixtures.
This is where a broad API can earn its keep. Infrai puts sending, suppression handling, event retrieval, domain verification, and DKIM rotation behind one REST API and one key; the wider surface currently spans 295 routes across 20 modules. No SDK is required, and public, keyless discovery describes request and response schemas, billing, and runnable examples in 10 languages. That removes schema guesswork when a notebook experiment becomes a small production worker. The limitation is substantial: email events are pulled rather than pushed. It is not suitable when immediate webhook delivery, SMTP migration compatibility, or voice, WhatsApp, or RCS from the same provider is required; use a provider built around those needs instead.
Build the polling evidence loop first
The following Python program calls the verified event-list route, honors Retry-After on HTTP 429, rejects other HTTP failures, and appends the unmodified response to a JSON Lines evidence file. It deliberately does not guess event field names. After inspecting the discovery schema for your deployed capability, put a schema-specific projector between this archive and the suppression updater.
Run it with INFRAI_API_KEY and EMAIL_EVENTS_URL set, using the verified event-list URL from your capability discovery result. Keeping the URL in deployment configuration also prevents an unreviewed endpoint change in source. A cron job can invoke it at the interval your response-time objective allows; keep a cursor only after the corresponding raw snapshot has been durably written.
import hashlib
import json
import os
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
EVIDENCE_FILE = Path("email-event-evidence.jsonl")
MAX_ATTEMPTS = 5
def retry_delay(value: str | None, attempt: int) -> float:
if value:
try:
return max(0.0, float(value))
except ValueError:
try:
retry_at = parsedate_to_datetime(value)
now = datetime.now(retry_at.tzinfo or timezone.utc)
return max(0.0, (retry_at - now).total_seconds())
except (TypeError, ValueError):
pass
return min(30.0, (2**attempt) + random.random())
def fetch_events(api_key: str, events_url: str) -> tuple[bytes, int]:
request = Request(
events_url,
method="GET",
headers={"Authorization": f"Bearer {api_key}"},
)
for attempt in range(MAX_ATTEMPTS):
try:
with urlopen(request, timeout=30) as response:
return response.read(), response.status
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == MAX_ATTEMPTS - 1:
raise RuntimeError(f"event polling failed: HTTP {error.code}: {body}") from error
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
raise RuntimeError("event polling exhausted all attempts")
def append_evidence(raw_body: bytes, status: int) -> None:
parsed_body = json.loads(raw_body)
record = {
"polled_at": datetime.now(timezone.utc).isoformat(),
"http_status": status,
"sha256": hashlib.sha256(raw_body).hexdigest(),
"response": parsed_body,
}
with EVIDENCE_FILE.open("a", encoding="utf-8") as output:
output.write(json.dumps(record, separators=(",", ":"), sort_keys=True) + "\n")
def main() -> None:
api_key = os.environ.get("INFRAI_API_KEY")
events_url = os.environ.get("EMAIL_EVENTS_URL")
if not api_key or not events_url:
raise RuntimeError("INFRAI_API_KEY and EMAIL_EVENTS_URL are required")
raw_body, status = fetch_events(api_key, events_url)
append_evidence(raw_body, status)
if __name__ == "__main__":
main()
Archive first.
This is intentionally a boring worker. Good. Network retries happen only for rate limiting, errors surface with their real response body, and the raw bytes are hashed before parsed data is serialized. A production deployment also needs exclusive cursor ownership, encrypted storage, retention rules, and metrics for poll age. Use an eval fixture containing a delivery, a soft bounce, a hard bounce, a complaint, a duplicate, and an out-of-order event; then assert that replaying the fixture twice produces the same recipient state. The easy mistake is advancing the cursor after parsing but before the archive write. Reverse those operations: a duplicate snapshot can be reduced idempotently, while a skipped response cannot be reconstructed from local state.
Polling changes the operational contract. If the worker runs every five minutes, the suppression decision can be at least that stale, plus provider processing time. Record last_successful_poll_at and alert on its age. Never interpret an empty response as proof that all mail was delivered.
Five minutes matters.
Compare the operational contracts, not the price cards
Amazon SES, SendGrid, Mailgun, and Postmark are all real alternatives, but their integration shapes differ in ways that matter more than a temporary unit price. The table focuses on the evidence transport and migration boundary documented by each provider. Verify regional availability, data-processing terms, and current account requirements directly before procurement.
| Product | Evidence path to evaluate | Practical fit | Boundary to test early |
|---|---|---|---|
| Amazon SES | Event publishing through AWS destinations | Teams already operating AWS event and identity controls | The AWS service graph and permissions become part of the email system |
| SendGrid | Event Webhook plus suppression APIs | Teams wanting pushed events and a broad email feature set | Webhook verification, replay handling, and account-specific retention |
| Mailgun | Webhooks plus an Events API and suppression resources | Teams wanting both push processing and later event queries | Region selection and the exact retention window for searchable events |
| Postmark | Delivery, bounce, and spam-complaint webhooks with message streams | Teams separating transactional streams and favoring a focused email product | SMTP or API migration details and stream-level policy |
| Poll-based unified API | Event polling with suppression and domain operations in one contract | Teams accepting lightweight polling to reduce separate integration surfaces | No email event webhooks or SMTP relay |
The choice is straightforward for the marketplace example. Pick the polling contract when five-minute-class evidence latency is acceptable and a worker already exists. Favor SendGrid, Mailgun, or Postmark when immediate pushed email events remove meaningful business risk. SES is compelling when AWS-native event routing and IAM operations are already normal work for the team. If an existing system speaks SMTP and cannot move to an HTTP send API, eliminate any option without SMTP support before building a proof of concept.
There is no honest universal winner. A startup sending transactional order notices has a different failure budget from a password-reset service or a high-volume promotional platform. Score the exact flow with production-shaped fixtures, not a feature-count spreadsheet.
Make the decision with replayable tests
Before selecting a provider, run the same six-case fixture through each candidate's sandbox or test mode. Measure no invented benchmark: record the observed time from provider acceptance to evidence availability, then keep the raw artifact that supports the number. Confirm that a duplicate event is harmless, an out-of-order delivery cannot erase a later complaint, and a failed poll leaves the cursor unchanged.
I would set the go/no-go rule before opening a pricing page. The chosen stack must verify the dedicated domain, expose machine-readable bounce and complaint evidence, support a durable suppression decision, and let the team export enough raw material to replay its state reducer. If any one of those checks fails, the stack does not meet the compliance-evidence requirement.
Prompt and model costs do not belong in this delivery loop. If an AI system drafts optional seller-facing text, pin the approved template revision and evaluate it separately; never let model output choose recipients, suppression state, or legal retention. That split keeps notebook experiments away from the production evidence path.
Operate it as an evidence system
The launch checklist is a short narrative, not a wall of boxes. Verify the domain and DKIM records, send only the transactional template, and attach a stable correlation ID to the application record. Confirm that the poller archives raw responses before advancing its cursor. Exercise rate limiting and a full provider outage. Replay duplicates and reordered fixtures. Then prove that hard failures and complaints stop the next order notification while soft failures follow the bounded policy your team approved.
Review access and retention with counsel for the US and EU jurisdictions you actually serve. Keep proof of domain control, template revisions, suppression decisions, poll health, and deletion execution. A delivery event is operational evidence, not consent, and a hash is integrity context, not a magic compliance certificate.
Finally, rehearse export and replacement. The best practical stack is the one whose evidence can survive a vendor change without rewriting the marketplace's meaning of complained or hard_failed.
Top comments (0)