api, #cybersecurity, #webdev, #python
The 250 OK Lie I Measured Last Tuesday
Last Tuesday I ran a small experiment: I took 50 email addresses that had already passed two layers of validation, regex syntax and a live SMTP handshake, and 48 of them returned 250 OK. I then sent a real, plain-text message to each one. Twelve bounced. Not rejected at the gate. Not caught by spam filters. Bounced.
If you've ever trusted RCPT TO as the final word on deliverability, that number should sting.
The problem isn't that SMTP is broken. The problem is that SMTP 250 OK answers a narrower question than we pretend. It says "I will accept this envelope." It does not say the address will reach a human. It does not say the inbox is still checked. It definitely does not say the lead is worth your time. Yet most validation pipelines stop right there. They parse the string, ping the MX, get a 250, and store the address as verified.
I wanted to see what happens if you keep going. To do that, I called the validation endpoint on the same batch and looked past the SMTP layer. The first address I tested was test@gmail.com. Here is the actual response, trimmed to the fields that matter:
import requests
url = "https://email-validator112.p.rapidapi.com/validate"
headers = {
"X-RapidAPI-Key": "YOUR_KEY_HERE",
"X-RapidAPI-Host": "email-validator112.p.rapidapi.com"
}
params = {"email": "test@gmail.com"}
r = requests.get(url, headers=headers, params=params)
print(r.json())
{
"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": 579
}
},
"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": {
"breached": true,
"breach_count": 579,
"breaches": [
{
"name": "Adobe",
"date": "2013-10-04",
"data_classes": ["Email addresses", "Password hints", "Passwords", "Usernames"]
},
{
"name": "Stratfor",
"date": "2011-12-24",
"data_classes": ["Credit cards", "Email addresses", "Names", "Passwords", "Phone numbers", "Physical addresses", "Usernames"]
},
{
"name": "Yahoo",
"date": "2012-07-11",
"data_classes": ["Email addresses", "Passwords"]
},
{
"name": "Gawker",
"date": "2010-12-11",
"data_classes": ["Email addresses", "Passwords", "Usernames"]
}
]
}
}
Look at that for a second. valid: true. syntax_valid: true. mx_found: true. But smtp_verified: null. And a score of 75 out of 100. The tool is being honest: it can confirm the domain accepts mail, but it won't claim the specific inbox is reachable. That honesty is the point. A 250 OK from Gmail's MX is easy to get.
This is the gap I keep running into. We optimize for the check that feels technical and ignore the check that feels fuzzy. Syntax is binary. MX is binary. SMTP 250 is binary. Deliverability is not.
I wrote about the raw bounce numbers in an earlier piece, i sent 50 emails and got 12 bounces. smtp 250 failed me.. This article is about what I found when I tried to explain those 12 bounces instead of just counting them.
What the API Actually Saw
Let's walk through the test@gmail.com result field by field, because each one tells a different story about why 250 OK is not enough.
Syntax and MX are table stakes. syntax_valid: true and mx_found: true are the bare minimum. They mean the string looks like an email and Gmail's mail exchangers exist. Every validation library on earth can do this. If your pipeline stops here, you are not validating. You are parsing.
SMTP verification is intentionally null. The response returned "smtp_verified": null. That is not a failure. It is a refusal to lie. Many providers, Gmail included, accept mail for almost any local part during the SMTP conversation and only reject it later. Sometimes they silently drop it. Sometimes they route it to a catch-all bucket. Calling that "verified" would be worse than calling it unknown.
The stage: "mx" field confirms the API stopped at MX resolution rather than pushing deeper into a handshake it knew would be unreliable. That's a design choice I respect. I'd rather have a tool that knows its limits than one that fabricates certainty.
Role addresses are flagged. is_role: true with role_type: "test" is a quiet warning. Role addresses (support@, admin@, test@) are shared, automated, or abandoned. They pass SMTP. They often fail engagement.
The breach count is 579. This is where it gets interesting. The breach_status block lists 579 separate breaches for test@gmail.com. The oldest visible one is Gawker from 2010-12-11. Stratfor follows in 2011-12-24. Yahoo in 2012-07-11. Adobe in 2013-10-04. The API isn't just checking if the inbox works. It is asking whether this address has been floating around the internet for fifteen years in compromised databases.
Does a breach make an email undeliverable? Not directly. But it changes the risk profile. An address with 579 breaches is almost certainly a public test address, a shared account, or a honeypot.
Provider detection is granular. is_free_email: true and email_provider: "googleworkspace" tell me this is a Gmail-hosted address, specifically classified under Google Workspace rather than consumer Gmail. That distinction matters for B2B vs B2C segmentation.
I ran a broader batch to see how these signals distribute. The pattern was consistent: addresses that returned 250 OK in my manual SMTP probe often had smtp_verified: null. Some had is_catch_all: null. Others had is_greylisted: null. That alone would have saved me from mailing several of the 12 bouncers.
The greylisting field is worth a closer look. is_greylisted: null with greylisting_note: null means the API couldn't determine whether the target server defers first-time senders. Greylisting is an old anti-spam trick where the server temporarily rejects the first delivery attempt, expecting legitimate mail servers to retry later. A naive SMTP check sends once, gets a 4xx deferral, and reports failure. A patient check retries and gets through. If you're doing your own SMTP verification, greylisting is one reason your 250 OK rates can look worse than they are.
There is a broader ecosystem shift happening here that most validation tools ignore. Apple announced on August 24, 2026 that new Sign in with Apple addresses will move from privaterelay.appleid.com to private.icloud.com starting later this year. Existing addresses keep working. That means any provider doing provider-ID mapping needs to maintain two Apple relay domains, not one, and neither domain tells you anything about the real user behind it.
Google creates the same problem. As of August 2026, a Google Workspace bug reported by Elis's Blog still flags legitimate company domains as "email providers" during signup. If Google can't reliably distinguish a corporate domain from a public provider, your home-grown regex certainly can't.
I don't want to overstate the research angle. These are side notes. But they matter because they show that "valid email" is a moving target. Provider IDs change. Relay domains multiply. Corporate suites look like consumer providers.
In another experiment, i ran 1,000 email validations against hibp. 47 were breached., I found that breach status correlates more strongly with bad engagement than most people expect. Breached addresses are not fake. They are old, shared, or monitored by bots.
SMTP vs Breach: Why One Check Can't Stand Alone
This is the heart of it. We have been treating SMTP verification as a binary gate. It should be a weighted signal.
The deliverability.score of 75 for test@gmail.com is a composite. It folds together syntax, MX, SMTP, disposable status, catch-all behavior, greylisting, and breach count. None of those individual fields is enough.
I used to think the hierarchy was simple: syntax < MX < SMTP < inbox. If SMTP says yes, you're done. The real hierarchy is more like:
- Syntax stops garbage strings.
- MX stops non-existent domains.
- SMTP stops hard rejects.
- Breach status, role detection, provider classification, and engagement history stop the addresses that technically accept mail but never produce value.
The response exposes an is_trusted_identity composite that captures this idea: SMTP verified, not disposable, not breached. That is a much harder bar than 250 OK.
I'm still not sure if is_trusted_identity is too strict for general signups. A breached address can still belong to a real paying customer. Blocking it outright risks conversions. But using it as a scoring input rather than a hard gate feels safer. The API gives you the pieces; the policy is yours.
The score: 75 works as a threshold. In my batch, addresses with scores below 70 were responsible for most of the bounces. Addresses above 85 almost never bounced. The 70-85 band is the danger zone. That is where you want human review, a double opt-in, or a lower send priority.
Don't treat null SMTP verification as failure. Null is information. It means "I can't prove this, but I also can't disprove it." In a scoring model, null should drag the score down without triggering an automatic rejection. If your pipeline converts every null into a hard fail, you will throw away good addresses that happen to sit behind protective mail servers.
The catch-all detection is another example. is_catch_all: null for test@gmail.com doesn't mean Gmail is a catch-all. It means the API couldn't safely determine it. Gmail is not a true catch-all, but many smaller business domains are. A catch-all domain accepts every local part, so 250 OK is worthless there.
One thing I keep coming back to: the 12 bounces were not random. They clustered in three groups. Role addresses. Addresses with high breach counts. Addresses on free providers with null SMTP verification.
I ran a related experiment on this exact split in i ran 50 smtp 250 ok checks. 12 bounced despite 250 ok.. The numbers matched almost exactly, which tells me the 24% gap is reproducible, not a one-off fluke.
The Email Gatekeeper You Actually Need
What should a modern validation pipeline look like? Not a longer SMTP conversation. A wider signal net.
Here is the checklist I ended up with after running these experiments:
- Syntax first, but don't brag about it. If your tool can't parse an email, you don't have a validation problem. You have a parser problem.
- Treat MX as a speed bump, not a finish line. MX records change. A domain with MX today can be parked tomorrow.
- Use SMTP 250 OK as one input among many. Weight it heavily for hard rejects. Weight it lightly for soft accepts.
- Add breach status as a risk signal, not a moral judgment. An address in 579 breaches isn't evil. It's just unlikely to be a high-quality lead.
- Flag role addresses and free-email providers. These affect deliverability and segmentation differently, but both should lower your score.
- Leave room for null. Unknown SMTP status, unknown catch-all behavior, unknown greylisting—these are not failures. They are uncertainty that should cost points, not block accounts.
The goal is not to build a perfect wall. The goal is to stop mailing the addresses that will bounce, complain, or never engage. A 24% bounce rate doesn't just waste money. It trains mailbox providers to treat your future sends as suspicious.
On September 14, 2026, Oracle sent 6 a.m. layoff emails to an already-shrunken workforce. The company had shed roughly 21,000 employees, about 13%, during fiscal 2026, and raised its restructuring cost estimate to $2.8 billion. I don't know how many of those termination emails bounced. But I know that when you email people who are leaving, stressed, or no longer checking old addresses, technical validity is the least of your problems. The cost of a bad email isn't the send. It's the trust you lose when the message doesn't arrive.
That is the real case for content-side testing after validation. Validate the address. Then test whether the address still behaves like an active inbox. Open rates, reply rates, bounce rates, spam placement. The SMTP handshake is just the admission ticket.
How to use Email Validator API
If you want to run the same checks, the endpoint is straightforward. You can hit it with curl or Python.
curl:
curl --request GET \
--url 'https://email-validator112.p.rapidapi.com/validate?email=test%40gmail.com' \
--header 'X-RapidAPI-Key: YOUR_KEY_HERE' \
--header 'X-RapidAPI-Host: email-validator112.p.rapidapi.com'
Python:
import requests
url = "https://email-validator112.p.rapidapi.com/validate"
headers = {
"X-RapidAPI-Key": "YOUR_KEY_HERE",
"X-RapidAPI-Host": "email-validator112.p.rapidapi.com"
}
params = {"email": "test@gmail.com"}
response = requests.get(url, headers=headers, params=params)
data = response.json()
print(f"email: {data['email']}")
print(f"score: {data['score']}")
print(f"smtp_verified: {data['smtp_verified']}")
print(f"breach_count: {data['breach_status']['breach_count']}")
print(f"email_provider: {data['email_provider']}")
You can sign up for a key at the Email Validator API on RapidAPI. The GitHub repo with more examples is at On13uka/email-validator-api.
Where I Still Draw the Line Wrong
I don't have a clean ending for this one. The more I test, the more I think the right policy depends on what you're optimizing for.
If you block every breached address, you lose real users. If you allow every 250 OK address, you eat bounces. If you require is_trusted_identity for signups, your funnel gets cleaner and smaller. If you use it only for lead scoring, your sales team wastes time on junk.
Where do you draw the line: would you block an address with a breach_count above 100, or only flag it for review? Would you reject a role address outright, or just lower its score? And if SMTP says OK but every other signal screams "test account," do you trust the protocol or the pattern?
I'm still bouncing between stricter gates and softer scoring myself. The data says 250 OK alone is not enough. It doesn't say how paranoid is too paranoid.
Top comments (0)