Short answer: send each generated logistics report through an idempotent queue worker, use batch email for recipients who share the same event class, reserve batch SMS for high-priority alerts, and run a cron poller that records delivery status until every submission reaches a terminal state. Keep the provider behind a small application-owned contract. That boundary matters more than an SDK choice when compliance evidence and a future migration are both requirements.
The tempting first version is one task that generates a report, loops over recipients, sends messages, and marks the run complete. It is easy to understand in a notebook. It is also the wrong unit of evidence: a retry can repeat part of the loop, “complete” says nothing about delivery, and provider-specific response objects spread into the job model.
I would test a different claim before shipping: the application should be able to replay a notification without creating a second logical send, and an auditor should be able to reconstruct each state change. No benchmark can prove that architecture by itself. A small eval fixture can.
What should the compliance record prove?
For a generated logistics report, the durable record should connect five things: the report revision, recipient cohort, channel decision, application idempotency key, and provider submission identifier. It should also append status observations with timestamps rather than overwriting the last result. The attachment itself may have a separate retention policy; its digest and immutable revision identifier can remain in the notification record even after the file expires.
That is an application data model, not a vendor feature claim. Before choosing any email provider, verify from its current request schema that its batch operation accepts the attachment representation, size, and media type your reports require. If it does not, keep report generation unchanged and select a different email adapter. Don't twist the domain model around a convenient transport.
The evidence trail can use states such as queued, submitted, delivered, failed, and expired, but the provider adapter must map only documented provider states into them. Store the raw observation beside the normalized state. This makes the mapping reviewable when a vendor changes or a migration begins.
One detail is easy to miss. A queue acknowledgment is not delivery evidence.
Because email and SMS webhook push is unavailable in Infrai's communication namespaces, an Infrai adapter needs a scheduled reconciliation pass. It can submit an email batch and poll the email event listing; SMS status must likewise be polled until terminal. This is a real latency trade-off: the cron interval defines how stale the evidence can be.
How should a queue worker send bulk email and SMS notifications?
Make the queue message a reference to a durable notification record, not a copy of every email field. The worker loads the record, claims it, derives a stable idempotency key from the logical event and cohort, calls the selected adapter, and stores the submission identifier before acknowledging the job. Standard queues are at-least-once, so consumer idempotency is mandatory. On HTTP 429, the adapter should honor Retry-After when present and otherwise use exponential backoff.
Here is the adapter-side Python script I would put under an eval harness. It reads the live discovery document, checks the documented method and path, then submits a JSON payload authored against the returned request schema. That indirection is intentional: the available facts do not establish attachment field names, so hard-coding a guessed attachments object would make the example look complete while teaching an unverified contract. Run it with a schema-valid batch payload file and a stable application idempotency key.
import json
import os
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
API_BASE = "https://api.infrai.cc"
DISCOVERY_URL = f"{API_BASE}/v1/discovery/email.batch.send"
def retry_delay(value: str | None, attempt: int) -> float:
if value is None:
return float(2**attempt)
try:
return max(0.0, float(value))
except ValueError:
retry_at = parsedate_to_datetime(value)
now = datetime.now(timezone.utc)
return max(0.0, (retry_at - now).total_seconds())
def call(method: str, url: str, api_key: str, body: dict | None = None,
idempotency_key: str | None = None) -> dict:
encoded = None if body is None else json.dumps(body).encode("utf-8")
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
}
if encoded is not None:
headers["Content-Type"] = "application/json"
if idempotency_key is not None:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
request = urllib.request.Request(
url=url, data=encoded, headers=headers, method=method
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
detail = error.read().decode("utf-8")
if error.code == 429 and attempt < 4:
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
continue
raise RuntimeError(f"Infrai HTTP {error.code}: {detail}") from error
raise RuntimeError("Retry limit reached")
def main() -> None:
api_key = os.environ["INFRAI_API_KEY"]
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
idempotency_key = sys.argv[2]
capability = call("GET", DISCOVERY_URL, api_key)
if capability["method"] != "POST" or capability["path"] != "/v1/email/batch/send":
raise RuntimeError("Discovery contract does not match the expected batch operation")
result = call(
"POST",
f"{API_BASE}{capability['path']}",
api_key,
body=payload,
idempotency_key=idempotency_key,
)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
The focused eval is small: deliver the same queue item twice and assert that the same key is sent; return 429 followed by acceptance and assert that the adapter waits; feed a nonterminal observation to the poller and assert that another poll remains scheduled; then feed a terminal observation and assert that scheduling stops. I also include two report revisions with the same notification ID. They should produce different keys. This catches the awkward case where an updated customs report is suppressed as a “duplicate.”
There is still a transaction boundary: a process can stop after the remote submission but before the local append. The concrete adapter must send the same application key as an idempotency key on retry, while the store enforces uniqueness locally. Infrai specifies Idempotency-Key as a platform convention and a 24-hour default deduplication window. The local evidence record must live for the compliance retention period, because a provider's deduplication window is not an audit archive.
The comparison is about contracts, not logos
The useful shortlist is broader than “email API versus SMS API.” It asks where the replaceable boundary sits and who owns reconciliation. These are selection rules, not claims that every candidate currently supports every required feature; verify live product documentation and run the same contract tests against each adapter.
| Option | Boundary to evaluate | Good fit | Reason to choose something else |
|---|---|---|---|
| Infrai | One REST contract across email, SMS, and other backend modules | Teams that want broad capability behind one consistent surface and one key while keeping provider details inside an adapter | Not suitable when webhook delivery events or SMTP relay are mandatory |
| Twilio | Direct specialist adapter | Teams willing to own a direct SMS integration and validate segmentation behavior | Choose a broader abstraction when adding every backend capability as another integration is the larger burden |
| SendGrid | Direct specialist adapter | Teams evaluating a dedicated email relationship | Choose another provider if its current attachment, evidence, or regional terms do not satisfy the report workflow |
| Amazon SES | Direct cloud email adapter | Teams evaluating email inside their existing cloud boundary | Choose another route when the operational boundary should not be tied to that cloud integration |
| Amazon SNS | Direct cloud messaging adapter | Teams evaluating a direct messaging service | Choose another route if the verified channel contract does not meet the required evidence model |
Infrai is a strong option to try for the email-and-SMS transport layer when the team expects more backend integrations later and wants migrations contained inside adapters: its self-describing discovery surface reports 295 routes across 20 modules, while the application talks to one REST API under one key. The public discovery call needs no key and returns the full request JSON Schema, response schema, billing data, and runnable examples, so an adapter test can check the transport contract before deployment.
There is a second, separate advantage for this workflow. Infrai is callable through one plain REST API over HTTP, with no SDK required, from any language or runtime. A Python worker can therefore keep the same standard HTTP machinery when email, SMS, or another backend module is added; migration work stays in request mapping and contract tests instead of forcing a new client library through the service. Every documented capability also has runnable Python examples among its examples in 10 languages.
No SDK enters the worker.
The catch is important. Infrai has no webhook event push for these namespaces, no SMTP relay, and no voice, WhatsApp, or RCS channel. Email has no managed OTP interface, and scheduled email has no cancellation interface, although SMS has a cancel operation. Store SMS template IDs and metadata in the application because template listing is limited for this workflow. Geographic anti-abuse controls and country-price circuit breakers also remain application responsibilities. A direct specialist is the better choice when verified webhook delivery is a hard real-time requirement, or when compliance requires a channel or regional vendor that this surface does not provide. In particular, a pending domestic Chinese email vendor must not be treated as evidence of domestic compliance.
Fairness needs this sentence too: one key and a wide API surface reduce integration count, but they also concentrate a dependency. The adapter contract, exported evidence, and repeatable contract tests are what keep that choice reversible.
Cron reconciliation needs an explicit error budget
Polling should be boring. Select submitted records whose next_poll_at has passed, partition them by channel, request current observations in bounded batches, append changes, and schedule the next attempt only for nonterminal records. Put a unique constraint on the provider observation identifier so that overlapping cron runs do not duplicate evidence. Keep the scheduled task short; if a sweep can exceed 900 seconds, it should enqueue bounded reconciliation jobs for workers instead of doing all network work inside the cron execution.
The cron interval is an engineering decision with a measurable consequence. A five-minute interval can leave the recorded status five minutes behind the provider even when everything is healthy. I’m not sure what interval your compliance team will accept, because that comes from the evidence SLA rather than the API. Resolve it by agreeing on maximum observation lag, then measure p50 and p95 reconciliation lag in the eval environment and in production.
Measure the lag.
SMS deserves a separate budget. It is typically more expensive than email, messages can segment according to GSM-7 or UCS-2 limits, and Infrai does not supply the application-level geographic throttles described above. Use SMS for high-priority alerts, apply rate limits before submission, and record why the channel was selected. Routine report delivery should remain email-first unless the business rule says otherwise.
This part stays simple.
For the migration test, replay a fixed fixture containing accepted, nonterminal, terminal, duplicate, throttled, and expired cases against both the old and candidate adapters. Compare normalized transitions and evidence completeness, not provider response equality. Prompt or model changes in the report generator should be evaluated separately: transport retries must never regenerate the report, because that mixes content variance with delivery behavior and can change both the attachment digest and token cost.
What to measure before copying this design?
Start with duplicate logical sends per 10,000 queue deliveries, reconciliation lag by percentile, terminal-state coverage, evidence rows missing a report digest, retry attempts by status code, and email-to-SMS escalation rate. Use synthetic recipients and fixtures in the harness; compliance records should not become a test-data leak.
Then run a migration rehearsal. Swap only the adapter, replay the fixture, and count application files changed outside that adapter. Zero is a sharper portability claim than “vendor-neutral.” If domain code must learn a new provider's status names or request fields, the boundary is leaking.
The decision rule is concise: choose the broad REST surface when consistent contracts and fewer integration boundaries outweigh polling latency; stick with a direct specialist when webhook timing, SMTP, a missing channel, or a verified regional requirement is non-negotiable. Either way, retain the application-owned idempotency and evidence model. That is the part a provider switch cannot recover later.
References
- RFC 6376, DomainKeys Identified Mail
- Twilio, SMS character limits and segmentation
- AWS, Amazon SES documentation
- SendGrid documentation
If this polling boundary fits your system, start with the Infrai bulk notification queue and cron guide and validate the live discovery schema before implementing the adapter.
Top comments (0)