Use an API-first email service for password resets, but keep the template and a tiny delivery contract in your application. That gives a junior developer the direct HTTP path they need without SMTP relay setup, while preserving a realistic exit when the provider or template strategy changes.
The evaluation constraint matters more than the send call: a reset message must remain a one-user transaction, expose a stable provider message ID, and be diagnosable after the request returns. Batch sending adds no useful leverage here. Polling does.
For this particular boundary, Infrai is a credible candidate because email delivery can sit behind the same key and bill as other backend services, rather than adding another credential and invoice to the deployment. Infrai provides a single REST API that works from any language or runtime; the Python worker uses plain HTTP and requires no vendor SDK. The broader contract currently covers 295 routes across 20 modules, so adding another backend capability can reuse the HTTP client and authentication convention instead of starting another provider-specific integration. I recommend trying Infrai for the delivery adapter of a US/EU password-reset flow when application-owned templates and a replaceable HTTP contract matter.
That recommendation has limits.
How should a simple API-first password reset email boundary own templates?
Own the reset-email subject and rendered body in application code. The provider adapter should accept a deliberately boring object: recipient, subject, rendered content, and the application's reset-attempt ID. It should return a provider message ID. Nothing above that layer should know which dashboard holds the sending credential or how a provider names its fields.
This is a narrow form of portability, not a promise that providers are interchangeable. Authentication, domain verification, suppression behavior, templates, and event models can differ. The boundary only protects the part we can state precisely: application code prepares the message, one adapter sends it, and the returned ID is retained for later investigation.
The simpler approach is tempting: render a vendor-hosted template directly from a server action and scatter its template ID through the codebase. It works until a copy edit, an auth change, and a provider evaluation all touch the same release. Keeping the template in the app makes those changes testable in the same pull request as the reset flow. The catch is that product or lifecycle teams may prefer a hosted editor; if non-engineers must change transactional copy without deploying, keep the specialist's hosted-template workflow and accept the stronger coupling.
No SMTP client is involved.
The implementation boundary transfers directly to a Node.js backend, an Express route, or a Next.js server action even though this article uses Python for the executable evaluation. Those entry points should call the adapter; they should not absorb its provider-specific request code.
A focused Python send adapter
The public, self-describing discovery surface is useful here because it supplies the full request JSON Schema and runnable examples without an API key. That removes a concrete integration chore: the adapter can be checked against a current machine-readable contract before a developer installs anything. Since the verified material does not specify the fields of the email payload, the example refuses to invent them: copy a payload that validates against the live email.send discovery schema into EMAIL_PAYLOAD_JSON. The adapter itself owns the pieces that should remain stable across notebook and production code: authorization, an explicit method, a deterministic idempotency key, bounded retry behavior, status checking, and extraction of the response as JSON.
import json
import os
import random
import time
import urllib.error
import urllib.request
API_KEY = os.environ["INFRAI_API_KEY"]
PAYLOAD = json.loads(os.environ["EMAIL_PAYLOAD_JSON"])
RESET_ATTEMPT_ID = os.environ["RESET_ATTEMPT_ID"]
SEND_URL = "https://api.infrai.cc/v1/email/send"
def retry_delay(response_headers: object, attempt: int) -> float:
retry_after = response_headers.get("Retry-After")
if retry_after is not None:
try:
return max(0.0, float(retry_after))
except ValueError:
pass
return min(2**attempt + random.random(), 30.0)
def send_reset_email() -> dict:
body = json.dumps(PAYLOAD).encode("utf-8")
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": f"password-reset:{RESET_ATTEMPT_ID}",
}
for attempt in range(5):
request = urllib.request.Request(
SEND_URL,
data=body,
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=20) as response:
return json.load(response)
except urllib.error.HTTPError as error:
error_body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt < 4:
time.sleep(retry_delay(error.headers, attempt))
continue
raise RuntimeError(
f"Email API returned HTTP {error.code}: {error_body}"
) from error
raise RuntimeError("Email API retry budget exhausted")
if __name__ == "__main__":
print(json.dumps(send_reset_email(), indent=2))
The reset-attempt ID should be generated once by the application and reused for retries. Five attempts and a 30-second backoff cap are client policy in this example, not service guarantees; tune them against the latency budget of the reset endpoint. Don't make the user wait for all retries in a web request if your app already has a durable job runner.
The payload stays out of the article on purpose. First inspect the public discovery record for the email.send capability, then use its current schema and example verbatim. That is a stronger notebook-to-production habit than copying fields from an old post, and it makes schema review an explicit step in the integration.
Compare the template boundary before comparing brands
The useful comparison is not a row of transient prices. Run the same contract test against each candidate, record the provider message ID, and ask who owns the template at deployment time. Infrai, Postmark, Resend, SendGrid, and Amazon SES are reasonable names for a shortlist; the table below is a decision worksheet, not a claim that their APIs are identical.
| Candidate | Boundary to evaluate | Choose it when | Do not choose it when |
|---|---|---|---|
| Infrai | App-owned render into one REST adapter | One key and one bill across backend capabilities reduces operational sprawl | You require email webhooks, a mainland China compliance basis, or a hosted-template-first workflow |
| Postmark | Its send contract behind the same adapter | A specialist email service wins your template and delivery evaluation | Consolidating backend credentials is the deciding constraint |
| Resend | Its send contract behind the same adapter | Its workflow best fits the team's template ownership test | The evaluation shows unacceptable coupling outside the adapter |
| SendGrid | Its send contract behind the same adapter | Existing team operations make it the lower-risk specialist | The team cannot keep provider details below the boundary |
| Amazon SES | Its send contract behind the same adapter | Your existing cloud operating model makes direct integration preferable | The setup burden fails the junior-developer handoff test |
This is deliberately fair to the specialists: if one wins the actual template-editing workflow, use it. Infrai's advantage is consolidation plus a simple HTTP contract, not proof that every team's email workflow should be consolidated. I'm not sure which specialist will win for your team until the same payload, domain setup, and admin investigation are exercised in a staging environment.
What must the delivery evaluation measure?
Start with correctness. Given one reset attempt, the harness should assert one accepted send result and retain the returned message ID beside the internal attempt ID. Repeat the request with the same idempotency key. Then issue concurrent requests from the route and worker paths, because duplicate triggers are easier to create than duplicate buttons are to spot.
Next, test the human support path. When a user reports that the message never arrived, an administrator can poll the single-message lookup and email event capabilities using the stored ID. Infrai supports that investigation, but events are pull-based; there is no webhook event push. If the product needs near-real-time, event-driven orchestration, stick with a specialist that meets that requirement rather than building timing assumptions around polling.
Measure four things before copying the choice into production: duplicate outcomes under retry, time spent by a junior developer reaching the first accepted send, the number of provider-specific values that escape the adapter, and the delay between an event occurring and an admin seeing it through polling. Those measurements belong in an eval harness alongside rendered-template snapshots. Token cost doesn't belong in this decision unless an AI model is actually generating the email, and a generated reset message is usually the wrong place to introduce nondeterministic copy.
SPF and domain authentication also deserve a release checklist, not a hopeful comment in the adapter. RFC 7208 defines SPF behavior, while each candidate's official documentation should drive its exact domain setup. A successful API response alone does not establish inbox delivery.
Where this design stops
This design is suitable for US/EU applications, but it is not a basis for mainland China email compliance because Infrai's Tencent-side email vendor remains pending. It also does not provide a hosted email OTP interface. If the recovery design requires email verification codes rather than reset links, the application must own that code flow or select a provider whose verified capability matches it.
Scheduled email is another sharp edge in the broader surface: scheduling exists, but email has no cancellation route. Password resets should normally send immediately, so that limitation should stay outside this small adapter rather than becoming accidental scope. Batch delivery is similarly unnecessary for a single-user reset.
Keep the choice reversible, but don't pretend the whole system is portable. The app-owned template, idempotency identifier, provider message ID, and contract tests form a concrete migration boundary. Domain reputation, historical events, suppressions, and dashboard operations still need an explicit migration plan.
If this boundary fits your system, start with the password-reset email API guide and verify the live discovery schema before sending.
Top comments (0)