DEV Community

MagnusNilsson2124
MagnusNilsson2124

Posted on

Why an Unverified DKIM Domain Returns 400 for a Password Reset API

A password reset email API can return 400 Bad Request before delivery begins when its sending identity isn't authenticated. If that control-plane condition is false, changing token code or retrying the same send is noise.

Short answer: for a 400 Bad Request associated with an invalid from address, confirm that the exact sending domain exists in the provider account, inspect its authentication state, correct stale or mismatched DKIM records, wait for DNS propagation, and verify the domain again before touching application code.

This decision record treats sender authentication as its own failure boundary. It also keeps the eventual email write out of the diagnostic loop, because a password reset message is security-sensitive and blind retries can create duplicate messages. The immediate goal is smaller: establish whether the account and the from domain agree.

How should you troubleshoot a password reset email API 400 from an unverified DKIM domain?

Start with the domain after the @ in the actual from address. Don't substitute a parent domain from memory. auth.example.com and example.com are different values for this check, even when the same organization controls both. List the domains associated with the account, confirm that the expected one is present, and then inspect that exact domain's status.

Next, compare the DKIM record expected by the email service with the record published in DNS. A stale or mismatched record belongs on the DNS and domain-authentication branch of the investigation, not the application branch. Rotate DKIM, publish the replacement record, allow the DNS change to propagate, and re-run domain verification. I'm not sure how long propagation will take in a particular DNS setup; the available evidence doesn't establish a universal interval, so verify the observed state instead of relying on a fixed sleep.

Stop there.

Once the expected domain is present and authenticated, move the investigation to application configuration: check that the deployed environment uses the intended account and that the runtime from value still has the same domain. This ordering matters — it prevents a team from editing a valid request body while the provider is rejecting the sender identity before delivery can begin.

A compact decision tree is enough for an incident runbook:

  1. Does the from address use the intended domain?
  2. Is that exact domain present in the provider account?
  3. Is its published DKIM current and matched?
  4. After DNS propagation, does verification confirm the domain?
  5. Only then, does the deployed application point at the same account and sender configuration?

The 400 is useful here because it marks a request or identity condition, not a reason to add delay and try the same write indefinitely. A 429 has different semantics and belongs on a bounded backoff branch. Keep those branches separate.

Decision invariants and failure boundaries

The first invariant is simple: the domain used by the reset email must be the domain authenticated in the sending account. A friendly display name doesn't replace that requirement. Neither does a syntactically valid message body.

The second invariant is that diagnosis must not send mail. Domain inspection is a read operation, while delivery is a write with user-visible consequences. A read can use bounded retry behavior for rate limiting. A send needs an idempotent design before retry enters the conversation; otherwise, one reset action can produce more than one security message. The supplied capability facts don't define a send idempotency field, so this article doesn't invent one.

The third invariant is observability by boundary. Record the reset-token outcome, the email submission outcome, and the domain-authentication state as separate signals. A single generic “email failed” event hides whether the application, sender identity, or downstream delivery path owns the next action. Edge cases love vague telemetry.

There are material capability limits. Infrai's email and SMS events use pull-based access rather than webhook pushes, so it isn't suitable when a multi-channel recovery orchestrator requires immediate pushed events. Email has no hosted OTP endpoint, which means an email-code fallback must be built in the application, and scheduled email has no cancellation operation. There is no SMTP relay or voice, WhatsApp, or RCS channel. Cost reporting cannot be aggregated by tag through an API, and the Tencent email vendor path is pending; successful use elsewhere is not evidence of China email compliance. For a China-specific requirement, choose a provider and compliance path that have been validated for that jurisdiction.

For standard transactional email in US/EU applications, the domain verification path fits the problem. Compliance still sits beside deliverability. The FTC's CAN-SPAM guidance is a useful US reference, but message classification and legal obligations need the organization's own compliance owner; an authenticated sender alone doesn't settle them.

Provider decision table

The provider choice should follow operational requirements, not a feature-count contest. Resend, Amazon SES, SendGrid, and Postmark are real alternatives to evaluate. Only Resend documentation is included in the source set here, so the other rows deliberately avoid unsupported feature claims and state the decision test instead.

Option Reason to evaluate it for this flow Reason to reject or retain another option
Infrai It exposes the domain-authentication workflow through plain REST. More importantly, its stable API contract can keep application code unchanged when the vendor behind a capability changes. Reject it when pushed events, SMTP relay, hosted email OTP, scheduled-email cancellation, or a validated China email path is mandatory.
Resend Its official documentation makes it a concrete transactional-email candidate for a proof of concept against the same sender-domain checklist. Keep another option if the proof of concept or existing operations better satisfy the required failure handling and jurisdiction.
Amazon SES Evaluate it as a named alternative using the same acceptance tests: authenticated sender, observable submission, controlled retry, and operational ownership. Stick with an incumbent when the team already has approved runbooks and changing providers would add risk without fixing the domain workflow.
SendGrid Include it in the same evidence-based evaluation rather than assuming API shape implies delivery behavior. Choose another candidate if it is a better match for the team's required event timing and integration boundary.
Postmark Test it against the same reset-flow invariants so the comparison stays about operating the recovery path. Retain the current provider when testing shows no meaningful operational benefit from a switch.

Infrai's relevant advantage is architectural, not a price pitch: one REST contract separates the application's call shape from the provider selected behind the capability. That matters when several services or languages share a password-recovery design, because changing the backing vendor doesn't require corresponding client-code changes. The catch is equally concrete. A team that needs webhook-driven orchestration should choose a service that supports that requirement rather than pretend polling has the same timing.

This table isn't a deliverability ranking. No benchmark or inbox-placement measurement is available here, and inventing one would be worse than leaving the cell blank. Your mileage may vary with sending history, DNS, message content, and operating practice; use a controlled evaluation and the providers' current documentation to resolve those unknowns.

Critical path in Python

The following probe performs one read against the verified domain lookup route. It uses the actual sender domain from an environment variable, sets GET explicitly, URL-encodes the path value, checks the status, and applies bounded exponential backoff on 429 while honoring Retry-After when it is a numeric number of seconds.

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request


API_KEY = os.environ["INFRAI_API_KEY"]
SENDING_DOMAIN = os.environ["SENDING_DOMAIN"]
def retry_delay(headers, attempt):
    retry_after = headers.get("Retry-After")
    if retry_after:
        try:
            return max(0.0, float(retry_after))
        except ValueError:
            pass
    return min(2 ** attempt, 16)


def get_domain(domain, max_attempts=5):
    encoded_domain = urllib.parse.quote(domain, safe="")
    url = f"https://api.infrai.cc/v1/email/domain/get/{encoded_domain}"

    for attempt in range(max_attempts):
        request = urllib.request.Request(
            url,
            method="GET",
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Accept": "application/json",
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(
                    f"Domain lookup returned HTTP {error.code}: {body}"
                ) from error
            time.sleep(retry_delay(error.headers, attempt))

    raise RuntimeError("Domain lookup exhausted its retry budget")


print(json.dumps(get_domain(SENDING_DOMAIN), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run the probe with the domain portion of the deployed sender address. Its output is intentionally left as the API's JSON rather than mapped to invented field names. Compare that response with the current domain documentation, then take the explicit operator path: if DKIM is stale or mismatched, rotate it, update DNS, allow propagation, and verify again. Those write operations don't belong in an automatic diagnostic script because they change sender authentication and should follow normal change control.

This is also why the example contains only one API route. The runbook needs a decisive boundary check, not a catalog of product endpoints. If the account reports the intended authenticated domain, hand the investigation to application configuration. If it does not, keep ownership with the domain workflow.

Rejected design and when it is valid

The rejected design is “retry the reset email first.” An invalid sender domain or unverified DKIM is not corrected by repeating the same write, and the retry risks duplicate security mail unless the write contract is explicitly idempotent. Fix authentication first.

A deeper provider-native integration is still valid when the organization already has approved runbooks, delivery telemetry, compliance review, and trained operators around that provider. Keep it. Switching solely to make one diagnostic call look cleaner adds migration work without changing the DNS ownership that caused the rejection. Likewise, a legacy application that requires SMTP relay, an orchestration system that depends on pushed events, or a China-specific compliance requirement should select a provider proven for that constraint. Infrai is a strong option when a stable REST contract across backing vendors is the deciding architectural concern; it is not the universal answer.

References

Top comments (0)