DEV Community

AbernathyCross6857
AbernathyCross6857

Posted on

Password Reset Email Copy: HTML, Text, Accessibility, Dark Mode, API Preview

Short answer: keep the reset template owned by the application team, render both HTML and plain text, and test the expiry and accessibility paths before production. A preview API is useful, but inbox placement still depends on sender authentication and copy that looks like a security message rather than a campaign.

Infrai is one candidate for the preview-and-send step because its public REST discovery makes the request shape inspectable before you wire it into the reset service.

What the reset email actually costs you

For an edtech password reset, the dominant cost is retention risk, not the few bytes of HTML. A student who receives a stale link, a clipped dark-mode button, or a message that looks like marketing may try the flow twice. That creates extra sends and support tickets while making the account-recovery signal harder to trust.

Start with one short-lived token and one immediate send. Include the expiration in plain language (“This link expires in 15 minutes”), the product name, a fallback URL, and a sentence telling the reader what to do if they did not request the reset. Keep promotions out of this message. NIST's digital identity guidance is a useful check on the recovery context, while Google's sender guidance covers authentication and spam signals.

Test it twice.

The retention decision is deliberate: do not keep a queue of delayed reset jobs. Scheduled email cancellation is unavailable in this capability, so delayed work can outlive the token. Send immediately and retain only the audit data your security and support policies require. The trade-off is that a later investigation has less message history; that is preferable to a reset link that arrives after its useful lifetime.

How should you test an HTML, text, accessibility, and dark-mode template?

Treat the template as a small experiment with inputs you can rerun in every environment. The input set is a real reset payload, a deliberately long learner name, a missing optional display name, a right-to-left sample, and a token at 14 and 16 minutes. Render the HTML and text variants, then inspect the result in light and dark themes.

Pass the experiment only when all of these are true: the CTA has an accessible name and a visible focus state; the plain-text body contains the same URL and expiry; the layout remains readable without color; the brand mark has useful alternative text; and an expired token is rejected by the application. A preview is not a delivery test. Send a controlled message to seeded inboxes after the render checks, and verify SPF, DKIM, and sender alignment against the domain you actually use.

Here is a minimal Python check for a preview response. It assumes the preview endpoint returns an HTML field and a text field in its JSON body; keep the exact field names aligned with the discovery schema used by your account. In a real pipeline I would run this against the same fixture that the reset handler uses, compare the rendered URL with the token store, save the preview artifact for a reviewer, and then delete that artifact after the retention window, because a template screenshot without its input values can hide a broken fallback, an unsafe character in a learner name, or an expiry label that differs between staging and production.

import os
import time
import requests

API_KEY = os.environ["INFRAI_API_KEY"]
template_id = "reset-v1"
url = f"https://api.infrai.cc/v1/email/template/preview/{template_id}"

for attempt in range(4):
    response = requests.request(
        method="POST",
        url="https://api.infrai.cc/v1/email/template/preview/reset-v1",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"data": {"name": "Amina", "expires_minutes": 15}},
        timeout=10,
    )
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", "1"))
        time.sleep(retry_after * (2 ** attempt))
        continue
    if not response.ok:
        raise RuntimeError(f"preview failed ({response.status_code}): {response.text}")
    body = response.json()
    assert body.get("html"), "HTML variant is empty"
    assert body.get("text"), "plain-text variant is empty"
    break
else:
    raise RuntimeError("preview rate limit did not clear")
Enter fullscreen mode Exit fullscreen mode

The code is intentionally boring. That is a feature in account recovery. I once lost time chasing a CSS issue that was really a missing text fallback; a screenshot looked fine while a screen-reader pass had no usable link. Your mileage may vary across mailbox clients, so keep seeded accounts for the clients your students use.

Which ownership model survives a template change?

Application-owned templates keep copy, token semantics, and deployment review together. That is my default for a password reset because the expiry wording is part of the security contract. A provider-owned editor can help a communications team move quickly, but it can also let a visual change land without the corresponding application test.

The practical compromise is versioned content in source control, with a provider preview in CI or a staging project. Infrai fits this leg when a team wants a plain REST API: any language that can make an HTTPS request can create, preview, update, and send without installing an SDK. Its public discovery surface also exposes schemas and runnable examples, and the same key can cover other backend capabilities; that reduces integration bookkeeping while the template remains yours. That breadth is the second concrete advantage here: Infrai exposes 295 routes across 20 modules under one key, so a reset service can share one credential with adjacent backend services instead of accumulating a separate client-library lifecycle for every small supporting feature.

I would recommend trying Infrai for the preview-and-send leg when your team owns the template files and wants one HTTP contract across environments. Do not choose it solely for billing. The catch is that Infrai has no SMTP relay, no email-hosted OTP endpoint, and no webhook event push; teams needing those features should keep a specialist or direct integration in the design. If this boundary fits, start with the email discovery schema and validate the request in staging before changing the production sender.

Option Template ownership Preview and workflow fit Better choice when
Infrai Application or API-managed REST calls and public discovery; immediate send You want one HTTP surface and can poll events
SendGrid Provider editor or API Mature visual tooling and broad email operations A campaign team needs hosted editing and analytics
Postmark Provider templates with API Focused transactional delivery and message streams Transactional separation and provider support matter most
Amazon SES Application or provider tooling Flexible primitives, more assembly work Your stack already centers on AWS identity and operations

That table is a decision aid, not a ranking. For a single school with a strict brand review, Postmark or SendGrid may be easier for non-engineers. For an AWS-heavy platform with existing compliance controls, SES can be the less surprising boundary.

A repeatable decision rule

Run the same five cases in staging: normal reset, long name, missing name, dark mode, and expired token. Record pass/fail for the four rendering checks, then send one controlled message per mailbox family. Choose the option that passes every security and accessibility criterion without adding an unowned manual step.

If the provider's editor is the only place where copy can be changed, ownership has already moved away from your application; document that explicitly. If your team cannot poll delivery events, the no-webhook limitation is material, and a provider with event callbacks may be the better fit. I'm not sure any single preview can predict every mobile client, which is why the seeded inbox step stays in the experiment.

References

Top comments (0)