DEV Community

Onizuka
Onizuka

Posted on

SMTP 250 OK Is Not Validation. 24% of My Verified Emails Bounced

api, #security, #webdev, #discuss

On October 6, 2026, at 17:20 UTC, I asked an email validator to check test@gmail.com. It came back valid: true, mx_found: true, score: 75. But smtp_verified was null. That null is the whole story.

A few weeks earlier I had run a campaign where 50 addresses passed an SMTP probe with a clean 250 OK. Twelve of them still bounced. That's 24%. The server said "accepted" and the mailbox still didn't exist, was full, or was a catch-all black hole. I ran the check through the RapidAPI endpoint to see what a more honest validator would report.

Here is the exact call:

curl --request GET \
  --url 'https://email-validator112.p.rapidapi.com/validate?email=test%40gmail.com' \
  --header 'X-RapidAPI-Key: YOUR_RAPIDAPI_KEY' \
  --header 'X-RapidAPI-Host: email-validator112.p.rapidapi.com'
Enter fullscreen mode Exit fullscreen mode

The response didn't give me a false green light. It gave me a yellow light with a provenance graph. That difference is what this article is about.

The finding: 250 OK is not a mailbox guarantee

I used to treat SMTP validation like a binary light switch. You telnet to port 25, say RCPT TO:<user@domain.com>, and the server returns 250 2.1.5 OK. Green light. Ship it.

That assumption cost me a 24% bounce rate on a list that had already been "verified." I had trusted smtp 250 ok and got a 24% bounce rate. The addresses weren't syntactically wrong. Their MX records resolved. The receiving server even nodded politely. But polite isn't the same as deliverable.

This wasn't a one-off. I also documented the send in another post: i sent 50 emails. 12 bounced in 38 minutes despite 250 ok. The pattern was consistent. A clean SMTP handshake predicted nothing about whether the message would survive greylisting, a full mailbox, a role alias, or a catch-all domain.

The validator I ran on October 6 exposed the gap immediately. For test@gmail.com, the response stopped at the MX stage. stage: "mx". smtp_verified: null. is_catch_all: null. is_greylisted: null. The API didn't lie and claim the SMTP handshake succeeded. It reported what it knew and left the rest empty.

Most validation tools would rather return a confident true than a messy null. The messy answer is usually the honest one. Gmail's servers don't let strangers probe mailboxes. They greylist, rate-limit, or silently accept everything from unknown IPs. A validator that pretends to have completed the SMTP probe is often just guessing. I'd rather see the uncertainty.

The data: what test@gmail.com actually returned

Here is the response, truncated to the fields that matter:

{
  "email": "test@gmail.com",
  "valid": true,
  "stage": "mx",
  "syntax_valid": true,
  "mx_found": true,
  "smtp_verified": null,
  "is_disposable": false,
  "is_catch_all": null,
  "is_role": true,
  "role_type": "test",
  "score": 75,
  "deliverability": {
    "score": 75,
    "factors": {
      "syntax_valid": true,
      "mx_found": true,
      "smtp_verified": null,
      "is_disposable": false,
      "is_catch_all": null,
      "is_greylisted": null,
      "breach_count": 0
    }
  },
  "suggestion": null,
  "is_free_email": true,
  "email_provider": "googleworkspace",
  "is_greylisted": null,
  "greylisting_note": null,
  "normalized_email": "test@gmail.com",
  "is_plus_addressed": false,
  "breach_status": null,
  "breach_status_error": "HIBP_API_KEY invalid or unauthorized",
  "is_trusted_identity": null,
  "invalid_explanation": null,
  "syntax": {
    "valid": true,
    "local": "test",
    "domain": "gmail.com"
  },
  "mx": {
    "has_mx": true,
    "records": [
      {"priority": 5, "exchange": "gmail-smtp-in.l.google.com"},
      {"priority": 10, "exchange": "alt1.gmail-smtp-in.l.google.com"},
      {"priority": 20, "exchange": "alt2.gmail-smtp-in.l.google.com"},
      {"priority": 30, "exchange": "alt3.gmail-smtp-in.l.google.com"},
      {"priority": 40, "exchange": "alt4.gmail-smtp-in.l.google.com"}
    ],
    "best": "gmail-smtp-in.l.google.com"
  },
  "smtp": null,
  "catch_all_probe": null,
  "identity_graph": {
    "email": "test@gmail.com",
    "gravatar": null,
    "breach_count": 0,
    "first_breach_date": null,
    "last_breach_date": null,
    "domain": "gmail.com",
    "fetched_at": "2026-10-06T17:20:12.921985+00:00"
  },
  "provenance": {
    "syntax": {"source": "internal", "confidence": 1.0},
    "mx": {"source": "DNS resolver", "confidence": 0.95},
    "smtp_verified": {"source": "SMTP probe", "confidence": 0.9},
    "breach_status": {"source": "Have I Been Pwned", "confidence": 0.95}
  },
  "fetched_at": "2026-10-06T17:20:12.922008+00:00"
}
Enter fullscreen mode Exit fullscreen mode

Let's read it like a forensic report.

valid: true and score: 75 look positive. But the score is 75, not 100. The API is already telling us this is a C-student address. The missing 25 points come from the things it could not verify: SMTP, catch-all behavior, greylisting, and breach status. A score of 75 with four null fields is not a green light. It's a yellow light with a shrug.

stage: "mx" is the most important field. It means the validator never reached a reliable SMTP conclusion. It checked syntax and DNS, found five Gmail MX records with priorities 5, 10, 20, 30, and 40, and stopped. The best MX is gmail-smtp-in.l.google.com. That's real infrastructure. It doesn't mean test@gmail.com is actively read by a human. It means Gmail will accept mail for the domain.

is_role: true with role_type: "test" is a warning. Role addresses are mailing-list or test aliases. They can exist, but they rarely convert. A signup from test@, admin@, or support@ is usually a bot, a QA script, or someone who doesn't want personal mail. I treat role addresses as high-risk even when the SMTP probe says OK.

is_free_email: true and email_provider: "googleworkspace" are useful for segmentation. If you're doing B2B lead scoring, a Gmail address is a consumer identity. If you're doing B2C, it's fine. The provider ID gives you more than a boolean; it tells you which infrastructure you're dealing with. Google Workspace, Microsoft 365, ProtonMail, Zoho, and Yandex all behave differently under SMTP probes.

breach_count: 0 is comforting, but breach_status_error: "HIBP_API_KEY invalid or unauthorized" undercuts it. The API couldn't reach Have I Been Pwned, so the zero is a default, not a verified fact. The identity_graph still lists breach_count: 0, first_breach_date: null, last_breach_date: null. This is exactly the kind of footnote you need to read. A dashboard that only shows breach_count: 0 would mislead you. The validator surfaces the error, which is the honest move.

is_trusted_identity: null is the composite verdict. The API defines a trusted identity as SMTP verified plus not disposable plus not breached. Because the SMTP probe and breach check both failed, the composite can't be computed. It returns null instead of guessing. That's the design pattern I want everywhere.

provenance is the chain of trust. Syntax has confidence 1.0 because regex is deterministic. MX has 0.95 because DNS can cache stale records. SMTP verification has 0.9 because probes can be blocked. Breach status has 0.95 when the key works. The confidence scores are not decoration. They tell you which parts of the result are hard and which are soft.

This is the kind of detail a competitor can't copy from public docs. Most validators return a single valid boolean. This one returns a provenance graph with per-field confidence, a provider ID derived from MX records, and a composite identity score that collapses to null when data is missing. You can't fake that without building the same data pipeline. The source is available on GitHub if you want to see how the probes are wired.

Analysis: why 250 OK is the weakest link in the chain

Apple's September 15, 2026 security blog post about verified photography makes the same point in a different medium. They argue that a photorealistic image is no longer enough to prove an image is real. The C2PA standard attaches provenance metadata after capture, but Apple notes the approach is "vulnerable to compromise at any point in the editing chain, and a viewer has no way to detect such a failure." Email validation has the same problem. SMTP 250 OK is the photorealistic image. It looks like proof, but the chain of trust after the handshake can fail in ways the receiver can't see.

The SMTP transaction is a handshake, not a deposit receipt. When you probe a server, you're asking "will you accept mail for this address?" The server can say yes for many reasons that have nothing to do with whether the mailbox exists. Catch-all domains accept everything. Greylisted servers defer the probe and the validator interprets silence as success. Some providers return 250 OK for any RCPT TO during the initial connection, then drop the message later. A validator that stops at 250 OK is trusting the server at its word, and a dashboard that hides null fields behind a green checkmark is doing exactly the same thing as a C2PA metadata chain that looks trustworthy but can be compromised at any editing step.

Oracle's layoff emails, reported September 14, 2026, are a brutal reminder that email delivery is not the same as email receipt. Oracle sent termination notices at 6 a.m. to an unknown number of employees after its workforce had already fallen by roughly 21,000, or 13%, during fiscal 2026. Before the latest round the company employed about 141,000 people. The restructuring cost is now roughly $2.8 billion, $700 million more than earlier estimates. Those numbers don't tell us how many emails bounced. But they do tell us that even a company with Oracle's resources can hit inboxes at the wrong time, with the wrong message, and face public fallout. If your "verified" list is 24% bounces, you're not just losing deliverability. You're losing trust.

The iLands AI spam case, published September 11, 2026, shows the supply side of the problem. Ernie Smith received over a dozen messages in three days from bots using the iLands.app domain, each offering to do research for around $25. The messages were sent under named personas like "Leo Ashford." They passed basic filters because they came from a real domain with real MX records. A validator checking only SMTP would mark them deliverable. But they were unwanted, low-quality, and arguably fraudulent. Deliverability without identity quality is a feature for spammers.

Jacob Goldstein's October 3, 2026 post about custom domain email adds another layer. He had owned jacobg.co for over two years and tried Cloudflare Email Routing, Mailflare, Purelymail, and others before settling on a solution. His forwarded messages sometimes took several minutes to arrive. Sending from a custom address required extra work. The lesson is that email infrastructure is more fragile than it looks. A domain can have correct MX records, valid SPF, and still fail to deliver because of forwarding delays, greylisting, or provider-specific quirks. A 250 OK probe can't catch those failure modes.

My own failure was smaller but noisier. On July 15, 2026, the validator flagged a paying customer's address as a catch-all and our automation paused the drip campaign. It cost us three hours of manual review before we realized the probe was just noisy. There is no lesson here. Sometimes a catch-all probe returns a false positive on a legitimate vendor and you waste an afternoon. That's the cost of treating a soft signal as a hard rule.

All of this points to the same conclusion: smtp 250 ok means nothing. 12 of 50 validated emails still bounced. SMTP 250 OK is overrated as a validation signal. It is useful, but it is not sufficient. The honest validator is the one that returns null when it doesn't know.

Implications: what to do with a yellow-light result

If you're building a signup form, don't block users solely because an SMTP probe returned 250 OK. Use a composite score. In the response above, is_trusted_identity is null because two critical checks are missing. A signup from that address should get extra friction: require email confirmation, cap the number of trial invites, or flag the account for review.

Treat role addresses as risky. is_role: true with role_type: "test" is a dead giveaway. Even if the mailbox exists, it probably belongs to an automated test or a shared inbox. I downgrade role addresses by at least 20 points in lead scoring.

Treat free-email detection as segmentation, not rejection. is_free_email: true and email_provider: "googleworkspace" tell you this is a consumer Gmail account. For B2B campaigns, that's a lower-intent signal. For B2C, it's normal. Use the provider ID to tune your sending strategy. Google Workspace and Microsoft 365 have different rate limits, bounce codes, and spam-folder behavior.

Monitor breach status carefully. The breach_status_error in my response is a warning. If your HIBP API key is invalid, you're not checking breaches. A breach_count: 0 default is dangerous. Either fix the key or stop showing breach data until you can verify it. Breached addresses are more likely to be abandoned, forwarded to spam traps, or controlled by attackers.

Use syntax suggestions. The API can suggest corrections like gmial.com to gmail.com. My test didn't trigger a suggestion, but the feature matters because typos are the cheapest source of bounces. A user who mistypes their domain will pass a regex check and still never receive mail.

Finally, measure real bounce rate. No validator can replace post-send telemetry. If your verified list bounces above 5%, your validator is lying to you. My 24% proved that. The only way to calibrate a validator is to compare its predictions against actual delivery events.

How to use Email Validator API

The endpoint is a simple GET call. You can test it with curl:

curl --request GET \
  --url 'https://email-validator112.p.rapidapi.com/validate?email=test%40gmail.com' \
  --header 'X-RapidAPI-Key: YOUR_RAPIDAPI_KEY' \
  --header 'X-RapidAPI-Host: email-validator112.p.rapidapi.com'
Enter fullscreen mode Exit fullscreen mode

And a Python version using requests:

import requests

url = "https://email-validator112.p.rapidapi.com/validate"
querystring = {"email": "test@gmail.com"}
headers = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "email-validator112.p.rapidapi.com"
}

response = requests.get(url, headers=headers, params=querystring)
data = response.json()

print("valid:", data.get("valid"))
print("stage:", data.get("stage"))
print("smtp_verified:", data.get("smtp_verified"))
print("score:", data.get("score"))
print("is_role:", data.get("is_role"), data.get("role_type"))
print("is_free_email:", data.get("is_free_email"))
print("email_provider:", data.get("email_provider"))
print("breach_count:", data.get("breach_count"))
print("breach_status_error:", data.get("breach_status_error"))
print("is_trusted_identity:", data.get("is_trusted_identity"))
Enter fullscreen mode Exit fullscreen mode

You can sign up for the API at the RapidAPI listing. The source code and issue tracker are on GitHub.

The gap: what we still don't know

I'm still not sure if running aggressive catch-all probes is worth the reputation risk. Every SMTP probe leaves a trace. If your sending IP develops a history of probing and not sending real mail, some providers start treating you like a scanner. More probes mean more data, but also more chances to burn your IP reputation.

There's also the HIBP key problem. A breach check that fails silently can look like a clean bill of health. The API surfaces the error, which is good, but your application still has to decide what to do with a null breach status. Reject the signup? Allow it with a warning? There is no universal answer.

The Email Validator API is one of the few tools I've used that admits those gaps instead of papering over them. That honesty is why I'm using it to re-audit our lists.

What is the one deliverability check you always forget to run after the SMTP handshake returns 250 OK?

Top comments (0)