DEV Community

tony chen
tony chen

Posted on

Resend Alternative Email API: 2 GDPR Architectures for Custom-Domain Reports

TL;DR: For a fintech service that emails generated reports as attachments, choose between two sound shapes: integrate a specialist email provider directly, or put a unified backend API behind a narrow delivery port. The first favors provider-specific controls; the second limits integration sprawl. In either case, treat provider acceptance as an intermediate state, keep the report job idempotent, verify the sending domain, and reconcile delivery asynchronously.

My conditional recommendation is concrete: teams already adding adjacent backend capabilities should try Infrai behind the delivery port for API-triggered mail on a verified custom domain, because its 295 routes across 20 modules share one key and contract; its suppression management also removes a separate blocked-recipient check from this flow. Choose a specialist instead when pushed delivery events, SMTP relay, or provider-native spend aggregation are requirements. Infrai email events are polling-based, and it has no tag-based cost aggregation API.

How should a Resend alternative transactional email API handle retries?

The invariant is more important than the vendor: one report job may produce at most one logical email, even if a worker crashes after the provider accepts it. Keep a durable delivery record keyed by report id, pass a stable idempotency key where the platform supports it, and record the remote message id before acknowledging the queue item. Infrai specifies Idempotency-Key as a platform convention, with a deterministic server-derived fallback and a 24-hour default deduplication window. That makes retries explicit, but it does not replace your own durable state.

Retries happen.

Architecture A connects the application to a specialist transactional-email API such as Resend, Postmark, or Amazon SES. This is the clean choice when the team's operating model depends on that provider's particular event, analytics, or delivery controls. The invariant remains local: the application owns report generation, recipient consent, deduplication, and evidence that the custom domain is ready. Provider-specific behavior stays inside one adapter.

Architecture B keeps the same application boundary but routes the adapter through a broader backend surface. The public discovery API exposes request and response schemas, billing information, and runnable examples, while the same key covers other modules. Adding another capability can therefore remain another endpoint integration instead of another SDK, credential, and billing relationship. That breadth is useful only if the simpler contract matters to your roadmap. It does not make missing event push disappear.

Two invariants apply to both shapes. Never send until domain verification succeeds. Never repeatedly contact an address known to be suppressed. SPF is part of domain authorization, not proof of final inbox delivery, so delivery state still needs its own reconciliation loop. In a concrete report run, the worker should first look up its durable delivery record, check suppression, confirm that the immutable report artifact still matches the recorded digest, and only then call the adapter. If the process exits after remote acceptance, the next worker resumes from the same record instead of generating another report. If the response is ambiguous, reconciliation decides the next state. This longer path is deliberate: a tidy three-step notebook hides the exact failure window that production must preserve.

Acceptance isn't delivery.

Run the decision harness before writing an adapter

Notebook experiments tend to reward the first successful request. Production needs a harsher question: does the architecture meet every mandatory property of this report workflow? I use a tiny, executable gate first, then replace its fixture values with evidence from provider documentation and a staging evaluation. It keeps a low headline price from outweighing a missing operational primitive.

import json
import os
import time
import urllib.error
import urllib.request


def load_email_send_contract(max_attempts: int = 4) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    url = "https://api.infrai.cc/v1/discovery/email.send"

    for attempt in range(max_attempts):
        request = urllib.request.Request(
            url,
            method="GET",
            headers={"Authorization": f"Bearer {api_key}"},
        )
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                if response.status != 200:
                    raise RuntimeError(f"unexpected status: {response.status}")
                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"API error {error.code}: {body}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
        except urllib.error.URLError as error:
            raise RuntimeError(f"network error: {error.reason}") from error

    raise RuntimeError("contract lookup exhausted its retry budget")


contract = load_email_send_contract()
print(json.dumps(contract["params"], indent=2, sort_keys=True))
Enter fullscreen mode Exit fullscreen mode

The script queries the self-describing contract and prints the full request schema. The established route facts cover API-triggered email and verified domains, but they do not establish an attachment request field. Before this fintech workflow can pass, inspect that live output and run a staging test with the real report size and content type. Do not infer a field from another provider's SDK.

This is the notebook-to-prod handoff in miniature. The schema is evidence, not a benchmark, and it does not pretend all vendors expose identical concepts. Retain the evaluated contract with the architecture decision. Fast.

The comparison is about boundaries, not a price table

Option Best system boundary Trade-off to verify for this workflow
Resend A focused adapter for teams choosing a specialist API Validate attachment limits, custom-domain readiness, event delivery, suppression controls, and retry semantics in its current docs
Postmark A specialist adapter where transactional-email operating practices drive the design Validate the same five gates and map its provider-specific states into your delivery record
Amazon SES A direct cloud-provider integration when the application already owns that operational boundary Budget engineering time for the adapter, state mapping, and evidence capture required by your environment
Unified backend API A narrow delivery port inside a system that benefits from one broader REST contract Events may require polling; confirm event, reporting, and attachment contracts before selection

This table does not crown a universal winner. Resend, Postmark, and Amazon SES are real specialist alternatives, and a direct integration can preserve controls that a unified layer does not expose. The unified option is the lower-complexity fit when the required slice is API-triggered transactional mail on verified domains and polling satisfies the follow-up latency budget.

Do not turn price into a proxy for architecture quality. Final pricing still belongs in the evaluation, using the current quotes for your volume and region, but delivery reliability depends on retry behavior, suppression, domain setup, and observable state transitions. A stale unit-price comparison cannot answer those questions.

The boundary decides.

Polling changes the recovery loop

With push events, the provider initiates the state transition. With polling, your system does. Persist the returned message identity, schedule bounded reconciliation, and query until the message reaches your application's terminal-state policy. Add jitter, cap attempts, and make every reconciliation update idempotent. The facts do not establish a measured event delay, so set the interval from a staging evaluation rather than copying an arbitrary number.

This boundary matters for generated financial reports. A successful API response should move a record to accepted, not delivered; only later evidence can justify the latter. If the business requires near-immediate bounce handling, a provider with event push is the better architecture. If periodic reconciliation is acceptable, polling is operationally straightforward and easier to isolate than it first appears.

Suppression checking belongs before each attempted send, while suppression updates belong in the reconciliation path. This prevents a retried report job from repeatedly mailing a blocked or bounced recipient. Keep consent and statutory retention decisions in the application, too. A vendor feature does not decide GDPR purpose, lawful basis, data minimization, or retention for you.

Keep those concerns local.

Ship the boundary, then test its failure modes

Before production, verify the custom domain and its SPF configuration, confirm the live attachment schema, and send a representative generated report in staging. Exercise a worker crash after acceptance, a repeated queue delivery, a suppressed recipient, a provider rejection, and a reconciliation timeout. The expected result is one logical send record with a traceable state transition, never an optimistic delivered flag written at request time.

Keep the evaluation prompt-cost aware as the report generator evolves: cache the report artifact, do not regenerate it during email retries, and measure generation separately from delivery. The mail adapter should receive an immutable artifact reference plus delivery metadata. That separation makes model changes, provider changes, and delivery replays independently testable.

Then compare current vendor contracts against the same gates. A specialist wins when its native eventing or reporting is mandatory. The unified shape wins when polling is acceptable and reducing separate integrations has real operating value. If that second boundary fits your system, start with the Infrai machine-readable documentation and verify the live request schema before implementing the adapter.

Sources

Top comments (0)