Reset email deliverability is a per-user question, not a percentage

Why password-reset mail goes missing: account-scoped suppression, accepted vs delivered, and dead addresses. A Python triage script over the Infrai email routes.

Pick the provider that can tell you, in one call, what happened to one specific message to one specific person. For password resets that’s the whole of deliverability — an aggregate rate of 99.2% is meaningless to the user who can’t get into their account, and it’s meaningless to the support engineer holding their ticket. Infrai’s email surface answers that question with two free reads, and this piece walks the three ways a reset quietly disappears.

Everything below assumes SPF, DKIM and DMARC are already published, because without them a reset email is both more likely to be filtered and trivially spoofable. What it adds is the layer above authentication: suppression, bounce classification, and the awkward case where the address itself has stopped existing.

Failure one: a list you forgot you had

The most common cause of “the reset email never arrives” isn’t spam filtering. It’s that the address is on your own suppression list, put there weeks ago by an unrelated message, and every send to it is dropped before it reaches a mail server.

Ask directly:

curl -s https://api.infrai.cc/v1/email/suppression/check/noreply@example.com \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"
{
  "ok": true,
  "data": {
    "email": "noreply@example.com",
    "reason": "manual",
    "added_at": "2026-07-25T16:02:13.112374Z",
    "scope": "account",
    "attempt_count_blocked": 0,
    "suppressed": true
  }
}

Read scope carefully. It’s account, which means one list covers every message this key sends — product notifications, receipts and resets alike. A hard bounce from a newsletter blocks the reset that the same person requests three months later, and no amount of DKIM hygiene changes that.

Suppression modelInfraiPostmark
Default scopeaccount-wide, one list per keyper-server, split by stream
Separate transactional streamnoyes, built in
Read one addressGET /v1/email/suppression/check/{email}API call
Remove an entryDELETE /v1/email/suppression/delete/{email}API call

That first row is a genuine limitation of the Infrai surface, and it’s the one place where a specialist wins outright. Postmark splits transactional from broadcast streams so a marketing bounce there can’t take your reset mail down with it, and SES gets most of the way there with configuration sets. Buy one of them if your marketing volume is large and messy and the two streams genuinely have to be isolated — that separation is worth paying a per-stream price for.

The mitigation that works here is discipline rather than configuration: never send bulk mail on the key you use for account recovery, and screen before every reset send.

Failure two: accepted is not delivered

A send returning 202-shaped success means the platform took custody. It does not mean a mail server accepted the message, and it certainly doesn’t mean a human saw it.

Two routes answer two different questions. GET /v1/email/get/{id} gives you the current state of a message; GET /v1/email/event/list gives you the timeline behind that state, and it needs a message_id query parameter — call it bare and you get a 400 rather than an account feed.

curl -s "https://api.infrai.cc/v1/email/event/list?message_id=msg_Hd029dIYk7I6cdlWLbiRQal7" \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"
{
  "ok": true,
  "data": {
    "items": [
      { "type": "sent", "at": "2026-07-26T01:11:52.181134Z", "recipient": "user@example.com", "message_id": "msg_Hd029dIYk7I6cdlWLbiRQal7", "meta": { "vendor_message_id": "91d93002-0128-4baf-ab2e-1ad52fddd91c" } },
      { "type": "queued", "at": "2026-07-26T01:11:52.170569Z", "recipient": "user@example.com", "message_id": "msg_Hd029dIYk7I6cdlWLbiRQal7", "meta": { "vendor": "resend" } }
    ],
    "next_cursor": null,
    "count": 2
  }
}

Newest first, one row per state change. A timeline that stops at sent a few minutes after the send is the normal picture; one that stops at queued for ten minutes points at the platform, not the recipient’s mail server.

Failure three: the address is gone

Hard bounces on a reset are different in kind from hard bounces on a campaign. A campaign bounce costs you one impression. A reset bounce means a paying customer has no route back into their account, because the only credential they can prove is an address that no longer accepts mail.

Decide the policy before it happens, not during the ticket.

The triage script

This is what a support engineer should be able to run with an email address and nothing else. Python 3.11 or newer, pip install requests:

import os
import sys
from urllib.parse import quote

import requests

BASE = "https://api.infrai.cc"
KEY = os.environ.get("INFRAI_API_KEY")
if not KEY:
    raise SystemExit("INFRAI_API_KEY is not set")
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})


def get(path, **params):
    response = SESSION.get(f"{BASE}{path}", params=params, timeout=10)
    payload = response.json()
    if not response.ok:
        error = payload.get("error", {})
        raise RuntimeError(f"{path} failed: {error.get('code')} {error.get('message')}")
    return payload["data"]


def triage(address, message_id=None):
    report = {"address": address}

    blocked = get(f"/v1/email/suppression/check/{quote(address)}")
    report["suppressed"] = blocked.get("suppressed", False)
    report["suppression_reason"] = blocked.get("reason")
    if report["suppressed"]:
        report["verdict"] = "blocked before send — clear the entry or use another channel"
        return report

    if message_id:
        message = get(f"/v1/email/get/{message_id}")
        events = get("/v1/email/event/list", message_id=message_id)
        report["state"] = message["state"]
        report["vendor"] = message["vendor"]
        report["timeline"] = [f"{e['type']} @ {e['at']}" for e in events["items"]]
        last = events["items"][0]["type"] if events["items"] else "none"
        report["verdict"] = {
            "bounced": "address rejected the message — treat as unreachable",
            "delivered": "accepted by the receiving server — check spam placement",
            "sent": "handed to the vendor, no receiving-server verdict yet",
            "queued": "not yet handed off — platform side",
        }.get(last, f"unrecognised terminal event: {last}")
    else:
        report["verdict"] = "no message id — search your own send ledger first"

    return report


if __name__ == "__main__":
    print(triage(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None))

Every call in it is free and rate-limited, so running it on every reset ticket costs nothing.

Where the bounce feeds back

A bounce should change your user row, not just a list somewhere. Map it: a hard bounce marks the address unverified and forces the user down the recovery path; a soft bounce is a retry; a complaint means stop sending anything non-essential. The suppression list carries the reason, so this is a lookup, not guesswork.

Removal is deliberate and one call:

curl -s -X DELETE https://api.infrai.cc/v1/email/suppression/delete/noreply@example.com \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"

Do that for a manual or unsubscribed entry when the user asks. Do it for a hard bounce only if you have a reason to believe the mailbox is back, because the next send will bounce again and hurt the reputation of the domain your other resets depend on.

Sender identity, briefly

Reset mail from a shared sending domain lands, but it reads like phishing to anyone paying attention. Your own domain is the fix, and it sits above the entry plan by design: POST /v1/email/domain/verify carries minimum_tier: "pro" in the capability record and answers 402 PRO_REQUIRED on a standard account, as does any send with a custom from. It’s a declared plan boundary rather than something you discover in production — curl -s https://api.infrai.cc/v1/discovery/email.domain.verify shows you the tier before you write a line of code. Plan for the upgrade or plan to stay on the shared sender; those are the two honest options.

When email is the wrong channel altogether — the mailbox is gone, or the user is locked out of the account that receives it — the fallback isn’t a fresh vendor evaluation. POST /v1/sms/send runs on the same key you just used to triage, the audit line for the support ticket goes to POST /v1/logs/ingest, and both land in the same GET /v1/account/usage breakdown you’re already reading. No second account, no second contract, no second on-call rota for the recovery path.

Cost, and what stays free

Reads dominate this workflow and reads are free. The suppression check, the message state, the event feed and the account listing are all billed at nothing and don’t touch the new-account trial. Only the send itself is metered: per email, at a catalogue rate of $0.00046 read on 2026-07-27, with $2 of free credit on a new account. Rates move — cuts and campaigns are normal — so get today’s number rather than trusting this line:

curl -s https://api.infrai.cc/v1/discovery \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  | jq '.capabilities[] | select(.id | startswith("email.")) | {id, free: .billing.free, unit: .billing.unit}'

And what you actually paid, per capability, is in your usage record:

curl -s https://api.infrai.cc/v1/account/usage \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  | jq '.data.breakdown[] | select(.key == "email.send")'

Divide cost by calls there and you have the rate that matters to your finance team, which is the metered one rather than the catalogue one.

The trade-off across this whole design: no webhooks means you never expose an inbound endpoint, and it also means you find out about a bounce when you next poll. For reset mail — where somebody is sitting on the page waiting — polling one message on demand is the right shape. For a 200,000-recipient campaign it isn’t, and you’d be better off with a provider that pushes events to you.

References

Browse more email developer guides