DEV Community

BrockFletcher1438
BrockFletcher1438

Posted on

FastAPI Transactional Email API: SaaS Password Reset Setup Behind Payment Receipts

Short answer: For a US/EU developer-tools SaaS, choose the transactional email API that lets one FastAPI adapter send a payment receipt and a password reset message, authenticate the sending domain, reuse templates, and collect enough delivery evidence under a deliberate retention policy. A pull-based REST option fits an existing worker architecture; use a webhook-oriented provider when delivery events must arrive immediately, or keep an SMTP-oriented incumbent when changing the application boundary would create more work than it removes.

Start with what the bill actually contains. Vendor message charges are one line, but the maintained system also includes a credential, domain authentication, template ownership, a send adapter, event ingestion, support lookup, and deletion of old event data. That is six operational surfaces before counting a second provider. The dominant term during an integration can be engineering ownership rather than a published unit price, so the useful comparison is ownership cost = initial wiring + recurring event collection + support and compliance work. This article does not assign fictional dollar amounts to those terms.

The change that moves that term is a narrow, provider-neutral mail boundary shared by the settled-payment receipt and the password-reset flow. Keep the payment transaction out of the provider call, persist a stable internal command, and let a worker submit it. Then retain only the provider message ID, internal correlation ID, template version, submission time, normalized outcome, and outcome time for the approved investigation window. Consider a receipt sent on January 8 and questioned on March 20: support can still correlate payment, message submission, template revision, and normalized outcome, but it may no longer see a provider-specific field from the raw response if the approved investigation window has closed. That can make an old edge case harder to reconstruct. Keeping every payload forever would make that lookup easier, yet it would also preserve recipient addresses, provider-shaped metadata, and potentially careless template data long after they serve the application. Stop keeping reset secrets, rendered bodies, and indefinite raw event payloads. The retention job is part of the integration, with an owner and an alert, rather than a policy sentence nobody implements.

Less data means less hindsight.

The trade is still worthwhile when it is deliberate.

Count the integration you will still own

An easy send demo proves very little. The production path begins when payment settles, creates one durable receipt command, renders a reviewed template, submits it without accidental duplication, and later records a delivery outcome. Password reset can use the same transport boundary, but it has a separate security lifecycle: the application owns token generation, hashing, expiration, single use, and invalidation. A delivery status must never decide whether a reset token is valid.

No secrets in logs.

For each candidate, count deployment units, credential owners, template sources of truth, domain-verification steps, event consumers, support views, and retention jobs. SendGrid, Postmark, Mailgun, and Resend belong in the rehearsal because they are real alternatives, not decorative names in a feature grid. Test every candidate with the same domain, receipt template, reset template, controlled inbox set, and deletion rule. I'm not sure a static comparison can settle account-specific US/EU contractual requirements; current terms, processing locations, and the evidence required by counsel must resolve those questions.

Candidate What the rehearsal should establish Reason to choose another path
SendGrid The complete domain, template, send, event, and support workflow The validated integration leaves more machinery than the team can staff
Postmark Both a settled-payment receipt and a time-sensitive reset message Current account terms or event behavior miss a written requirement
Mailgun The migration cost when an existing mail boundary matters Familiarity does not offset a failed retention or regional requirement
Resend The adapter and evidence policy using the same test messages Migration adds more maintained surface than it removes
Pull-based unified REST API Contract discovery, direct sends, bounded event polling, and one credential boundary Webhooks, SMTP relay, or managed email OTP are mandatory

This table intentionally has no permanent winner. Deliverability is not a logo attribute, and “easiest setup” is not meaningful until the same team has authenticated its domain, rendered production templates, exercised suppression policy, and retrieved evidence for a support case.

How should a SaaS test domain verification, templates, and deliverability?

Make verified sending domains and DKIM launch gates. DMARC adds a published policy and reporting mechanism for message authentication alignment, but authentication alone does not prove inbox placement. Use the real sending domain and controlled US and EU inboxes; exercise a long company name, missing locale, plain-text fallback, narrow display, expired reset link, and a recipient already covered by suppression policy. The order receipt is a clean baseline because payment settlement is durable, while password reset adds urgency and secret-handling constraints.

Keep the reset credential out of subjects, analytics fields, and logs. Escape user-controlled display values. Record template versions so support can identify what was sent without retaining the rendered message forever. Those details sound fussy until the German legal footer moves the call to action several screens down, or a support export turns a short-lived token into a long-lived secret. Your mileage may vary across mailbox providers and recipient populations, which is exactly why a controlled rehearsal beats a generic deliverability score.

There is also a channel boundary worth stating plainly. The evaluated unified REST capability has reusable email templates and direct email sending, but no managed email OTP API. An emailed code therefore needs application-owned code generation, storage, expiry, retry limits, and consumption rules, just like a reset token. The browser WebOTP API concerns specially formatted SMS messages and browser-assisted code entry; it does not provide email OTP semantics.

Treat pull-only delivery events as an architecture choice

Pull-only delivery and engagement events are a fit when the SaaS already runs scheduled workers. They are not a free substitute for webhooks. A simple upper bound is observed messages * scheduled checks per message; stop polling after a terminal outcome, use wider intervals for receipts than for reset messages, and retain a cursor or equivalent durable progress marker according to the discovered contract. On 429, honor Retry-After when present and apply bounded exponential backoff.

Immediate is not free.

Polling trades an internet-facing event receiver for scheduled reads and later visibility. Webhooks trade those reads for receiver authentication, replay protection, and durable ingestion. If immediate bounce or complaint automation is a hard requirement, stick with a provider whose currently verified webhook behavior meets it. If the application can only speak SMTP, keep an SMTP-capable option: the unified REST capability has no SMTP relay. It is also unsuitable when voice, WhatsApp, or RCS must share this workflow.

For a payment receipt, delayed delivery evidence may be operationally acceptable because payment truth lives elsewhere. For a waiting reset user, delay is more visible. Do not turn that pressure into blind resends: transport acceptance, delivery evidence, and token consumption are separate facts, and a resend policy needs its own abuse controls.

Read the contract before writing the adapter

A self-describing API changes the first task from installing and exploring an SDK to reading the method, path, full JSON Schema, billing metadata, and runnable examples. Infrai's concrete advantages here are a public discovery surface for 295 capabilities across 20 modules, runnable examples in ten languages, and one REST API that works through plain HTTP with no SDK to install; a single key and one bill cover those backend capabilities, so adding mail does not create another credential, invoice, and SDK ownership path beside the receipt worker.

The following Python program reads the declared contract for direct email sending. Discovery is public and needs no key. It sets the method explicitly, checks the status, surfaces a 4xx body, and backs off on 429; it deliberately does not invent a send body because the returned schema and runnable example define that body.

import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen


base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
api_key = os.environ["INFRAI_API_KEY"]
url = f"{base_url}/discovery/email.send"

for attempt in range(5):
    request = Request(
        url,
        method="GET",
        headers={"Authorization": f"Bearer {api_key}"},
    )
    try:
        with urlopen(request, timeout=15) as response:
            contract = json.load(response)
        break
    except HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        if error.code != 429 or attempt == 4:
            raise RuntimeError(
                f"Discovery returned HTTP {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)
else:
    raise RuntimeError("Discovery retry budget exhausted")

expected = {"method": "POST", "path": "/v1/email/send"}
actual = {"method": contract["method"], "path": contract["path"]}
if actual != expected:
    raise RuntimeError(f"Unexpected contract: {actual}")

print(json.dumps({
    "method": contract["method"],
    "path": contract["path"],
    "params": contract["params"],
    "examples": contract["examples"],
}, indent=2))
Enter fullscreen mode Exit fullscreen mode

Build the FastAPI adapter from that discovered schema. For the eventual write, read the key from INFRAI_API_KEY, send Authorization: Bearer <key>, use an explicit POST, attach a stable Idempotency-Key, inspect every response status, and expose the reason in a 4xx body to the calling worker. The platform convention specifies a 24-hour default deduplication window, but the application still needs its own durable command identity because a business retry can outlive that window.

This is a strong fit when a team values direct HTTP, contract discovery, and a shared credential boundary, and can poll GET /v1/email/event/list from infrastructure it already operates. It is not suitable when event push, SMTP compatibility, or a managed email OTP flow is non-negotiable. That limitation matters more than the pleasant first send.

Make the retention decision explicit

Before launch, write down the outcome vocabulary, event polling intervals, terminal states, maximum investigation window, deletion job owner, and the fields support may view. Verify domain authentication before production sends, and separate mail evidence from payment state and reset-token state. For a receipt, the internal payment ID should correlate the workflow without putting payment details into mail telemetry. For reset, store no recoverable credential in the delivery record.

Then rehearse failure at the application boundary: duplicate payment events, repeated reset requests, a 429 with Retry-After, a non-success 4xx body, a suppressed recipient, a late delivery outcome, and an expired reset token. These are not claims about a vendor incident. They are inputs a responsible adapter must handle.

The final decision rule is short. Choose the pull-based REST option when its discovery-driven integration removes a separate SDK and credential path, your worker can tolerate delayed event visibility, and application-owned reset-token logic is already part of the design. Choose a validated webhook provider for immediate event automation. Keep an SMTP-capable provider when SMTP is the boundary you cannot reasonably replace.

References

Further reading

Top comments (0)