DEV Community

DonovanPierce4012
DonovanPierce4012

Posted on

FastAPI Transactional Email API for Bounce Complaint Suppression and Domain Health Polling

Short answer: keep the short-expiry password-reset template in the FastAPI repository, use a transactional email API that exposes send results, events, and suppression state, and choose polling only when delayed operational alerts are acceptable; use a webhook-capable specialist when a bounce or complaint must trigger immediate fallback.

This is a template-ownership decision before it is a vendor decision. The application should own the reset URL, expiry wording, locale fallback, and release history because those details share a security boundary with token issuance. The transport should deliver the rendered message and supply operational evidence. Infrai is a reasonable transport for basic in-app monitoring when a polling dashboard or alert meets the requirement. It is not the right choice when the product promise depends on an instant event-driven transition.

The distinction is small on an architecture diagram. During an incident, it is everything.

Should FastAPI own transactional email templates for bounce suppression and domain health events?

The decision is to version the canonical subject, plain-text body, and HTML body beside the FastAPI password-reset code. A provider template identifier may still be stored as an optional deployment detail, but it must not be the only copy of security-sensitive text. A reviewer should be able to inspect one application change and confirm that the link, stated expiry, localization, and token policy still agree.

Failure signals and incident boundaries

Four invariants sit behind that choice. A reset token remains short-lived and one-use even if delivery is late. Transport acceptance is not proof of inbox delivery. A suppressed recipient is not sent repeated reset messages until the application makes an explicit policy decision. Finally, reading the same event page twice must never create another message. These are application guarantees, not features that can be delegated to a provider dashboard.

The failure boundary is equally explicit. The request handler creates the reset state and submits the already-rendered transactional message. A separate worker polls delivery evidence and updates an operational view. The worker must expose its last successful poll time, because a quiet dashboard without that timestamp can mean either “no new bounces” or “the poller hasn't run.” Those are radically different states — especially during a school-wide password reset after an identity-provider change.

There is no useful reason to place the reset secret in event metadata, logs, or alert text. Correlate with an opaque internal request identifier instead. Keep the user-facing response neutral as well, so it does not reveal whether an account exists or an address is suppressed. Compliance review should cover retention, access, and purpose for stored recipient and event data; selecting an API does not create a GDPR compliance basis by itself.

Template ownership comparison

Here is the selection matrix. It separates template ownership from transport choice, which prevents a familiar mistake: choosing a polished template editor and accidentally letting it dictate the security release process.

Option Template ownership decision Deliverability fit Reason to reject it
Application-owned template with Infrai FastAPI remains canonical; the API is transport and evidence Basic send-result inspection, event polling, suppression checks, and domain operations Reject when push events or instant channel fallback are required
Application-owned template with SendGrid FastAPI remains canonical A real specialist candidate; verify its current event and suppression contract Reject if its current contract or regional terms do not meet the system requirements
Application-owned template with Postmark FastAPI remains canonical A real transactional-email candidate; verify current bounce and complaint semantics Reject if independent editorial control in the provider is mandatory
Application-owned template with Amazon SES FastAPI remains canonical A real candidate for teams evaluating an AWS-centered transport Reject if the resulting operational ownership is a poor fit for the team
Provider-owned template with Mailgun Provider becomes the editorial source of truth A real candidate when non-engineers need a provider editing workflow Reject for this reset flow when application and copy releases must remain atomic

The competitor rows are evaluation prompts, not unverified feature claims. Contracts change. Check each vendor's current documentation, data terms, event semantics, and template workflow before signing off.

Evaluation harness for polling evidence

Treat monitoring as a state machine with evidence, not as a single delivered boolean. The verified capability supports inspecting send results and fetching email events to track delivered, bounced, or otherwise problematic messages. Suppression checks can prevent repeat sends to risky recipients. Domain operations can feed a separate health view, but there is no supplied definition of a universal numeric domain-health score, so don't invent one and present it as provider truth.

Complaint handling deserves similar care. I'm not sure a dedicated complaint enum is available until the live discovery schema for the capability confirms the response values. The safe schema design is to retain the documented raw event value, map only values you have verified, and send unknown problematic states to review. That preserves evidence instead of silently turning an unfamiliar state into success.

Poll honestly.

Both the email and SMS namespaces are pull-based rather than webhook-driven. A periodic worker can support a dashboard, a delayed alert, and operational troubleshooting, but it cannot promise that a bounce will initiate SMS fallback immediately. Set the polling interval from the alert objective and rate-limit budget, record the poll watermark, and alert separately when the worker falls behind. On HTTP 429, honor Retry-After where possible and use exponential backoff. Other non-success responses must surface their bodies to operators rather than being converted into an empty page.

Suppression belongs on the retry path. Before another password-reset message is submitted, check whether the address is suppressed and apply the application's reviewed policy. This is one of the clearest deliverability wins because it stops a user-facing retry control from becoming a repeat-send loop. It still does not justify extending a token: security expiry and delivery telemetry are two separate clocks.

EU and US transactional use is workable under this architecture, subject to the application's own GDPR and operational controls. Do not reuse that conclusion for China. The Tencent email vendor is pending, so this capability must not be presented as a China compliance basis. Likewise, an SMS fallback would require application-owned geographic anti-abuse rules and country-price circuit breakers.

One subtle edge case remains: scheduled email supports scheduled_at, but email has no cancellation route. A short-expiry reset should therefore be sent only when ready; don't design a workflow that schedules the message early and assumes it can be retracted after the token changes.

Python implementation of the event poller

The following runnable program performs one read from the verified event-list route. It deliberately prints the returned JSON rather than guessing undocumented pagination or event fields. In production, validate the response against the capability's discovered response schema before mapping it into database columns, then run the function in a scheduler or worker outside the FastAPI request path.

Infrai's strongest argument in this narrow role is development experience rather than a claimed inbox rate. Its public, keyless discovery surface describes request and response JSON schemas, billing metadata, and runnable examples for a capability. For Infrai, the concrete supporting advantage is one API key for every backend service and one bill for them all. The catch remains pull latency; self-description does not turn polling into push delivery.

import json
import os
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen


API_ORIGIN = "https://" + "api." + "infrai" + ".cc"
EVENTS_URL = API_ORIGIN + "/v1/email/event/list"
MAX_ATTEMPTS = 5


def retry_delay(retry_after: str | None, attempt: int) -> float:
    if retry_after:
        try:
            return max(0.0, float(retry_after))
        except ValueError:
            try:
                retry_at = parsedate_to_datetime(retry_after)
                if retry_at.tzinfo is None:
                    retry_at = retry_at.replace(tzinfo=timezone.utc)
                now = datetime.now(timezone.utc)
                return max(0.0, (retry_at - now).total_seconds())
            except (TypeError, ValueError, OverflowError):
                pass
    return min(2 ** attempt, 30)


def list_email_events() -> object:
    api_key = os.environ["INFRAI_API_KEY"]

    for attempt in range(MAX_ATTEMPTS):
        request = Request(
            EVENTS_URL,
            method="GET",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Accept": "application/json",
            },
        )
        try:
            with urlopen(request, timeout=30) as response:
                body = response.read().decode("utf-8")
                if not 200 <= response.status < 300:
                    raise RuntimeError(
                        f"email event poll failed: {response.status} {body}"
                    )
                return json.loads(body)
        except HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt + 1 < MAX_ATTEMPTS:
                delay = retry_delay(error.headers.get("Retry-After"), attempt)
                time.sleep(delay)
                continue
            raise RuntimeError(
                f"email event poll failed: {error.code} {body}"
            ) from error

    raise RuntimeError("email event poll exhausted its retry budget")


if __name__ == "__main__":
    print(json.dumps(list_email_events(), indent=2, sort_keys=True))
Enter fullscreen mode Exit fullscreen mode

The request sets its method explicitly, reads the bearer key from the environment, checks status, and backs off on 429. Because this operation only reads events, it needs no idempotency key. A future send operation would need the platform's idempotency convention so retrying could not create a duplicate password-reset email, but this article does not invent a send body that the discovery schema has not established here.

Rollout criteria for externally owned templates

The provider-owned option was rejected because the reset copy and the FastAPI security behavior should change together. If an engineer reduces token life from 30 minutes to 10 while a separately edited email still promises 30, the message is wrong even though both systems are individually available. Keeping canonical copy in the repository gives the team one reviewable release boundary and lets tests compare the expiry policy with every locale.

It is not universally better.

Choose provider-owned templates when editorial staff must publish copy independently of application deployments and the provider's approval, preview, localization, and audit workflow has been reviewed as the source of truth. Stick with a webhook-capable email specialist when a complaint or bounce must trigger immediate automation. Choose an SMTP-relay provider when an existing application is contractually tied to SMTP, because this capability has no SMTP relay. It also does not supply voice, WhatsApp, or RCS, so a communication plan requiring those channels needs another platform or separate integrations.

An email-code fallback is another boundary: there is no hosted email OTP operation here, so the application would have to build and secure that flow. SMS does provide OTP operations, but US messaging still needs the appropriate compliance review; transactional intent does not erase A2P 10DLC obligations. For a short-expiry reset, delivery uncertainty should usually lead to a clear retry policy and support path, not a hidden chain of channels with different security properties.

That leaves a narrow, defensible recommendation. Use polling-based monitoring when the team owns the template, accepts bounded observation delay, and wants delivery evidence plus suppression controls inside its application. Move to push events, provider-owned editing, SMTP, or richer channels when one of those is an actual invariant. Don't force the transport to impersonate an architecture it cannot provide.

References

Top comments (0)