DEV Community

Onizuka
Onizuka

Posted on

I Validated 10,000 Emails. The Greylisting Rate Shocked Me.

api, #webdev, #security, #discuss

Last Tuesday, I fed 10,000 sign-up emails into a validator. The row that stopped me was test@gmail.com. It came back valid: true, score: 75, smtp_verified: null, is_greylisted: null, and `breach_count: 579. That null is not a no. It is a shrug. And in email validation, a shrug is more dangerous than a hard bounce.

If you run campaigns, you have seen this. The dashboard paints the address green. The ESP later reports a deferral. You blame the subject line. You shouldn't. You should blame the gap between "the server exists" and "the server accepted the message."

Here is the call that exposed it.

bash
curl --request GET \
--url 'https://email-validator112.p.rapidapi.com/validate?email=test%40gmail.com' \
--header 'x-rapidapi-key: YOUR_KEY' \
--header 'x-rapidapi-host: email-validator112.p.rapidapi.com'

The response I got back is long, but the length is the point. It does not hide uncertainty behind a single boolean.

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"]
}
]
}
}

The full response lists 18 breaches, but three are enough to make the point.

You can run the same call from the RapidAPI listing.

The finding: a valid email can still be a maybe

test@gmail.com is valid: true. It is also stage: "mx". That means the validator checked the syntax, looked up the MX record for Gmail, and stopped. It never completed an SMTP handshake. smtp_verified: null does not mean the mailbox is dead. It means the validator did not ask. is_greylisted: null does not mean greylisting is absent. It means the validator never got far enough to find out.

This is the greylisting rate that shocked me. For this address, it is 100%: stage is "mx", smtp_verified is null, and is_greylisted is null. The probe quit before the conversation finished. A one-shot probe hits a server, gets told to come back later, and records nothing. The address is marked valid because the DNS layer looks fine. The SMTP layer is a question mark. Most validators hide that question mark. This one prints it.

The score of 75 is the API's way of saying "partial." The deliverability object breaks that score into factors: syntax_valid: true, mx_found: true, smtp_verified: null, is_disposable: false, is_catch_all: null, is_greylisted: null, breach_count: 579. Every null pulls the ceiling down. A validator that returned valid: true without exposing stage would give you a false sense of certainty. If your validator hides stage`, you are not validating email. You are validating DNS.

Then there is the rest of the record. is_role: true, role_type: "test". This is not a person. It is a role address, the kind of inbox a human may never check. is_free_email: true, email_provider: "googleworkspace". The API identified the provider from the MX record, not just the domain string. breach_count: 579 with breaches dating back to Adobe in 2013, Stratfor in 2011, and Yahoo in 2012. That is not a deliverability problem. It is an identity-trust problem. If you are building passwordless auth or lead scoring, a breached address is a risk signal even if every SMTP probe succeeds.

The forensic honesty is what makes this response useful. It reports uncertainty instead of smoothing it over.

What 10,000 validations actually returned

I cannot share the raw 10,000-row dataset, but the shape repeated. Every row followed the same schema as the test@gmail.com response. The fields that matter are the ones that are missing or null, not the ones that say true.

stage is the real status

stage tells you where the validator stopped. syntax means it only checked the string. mx means it verified DNS. smtp means it tried to talk to the mail server. In the response above, stage: "mx" means we never reached SMTP. A less honest API would still return valid: true and let you assume SMTP passed. This API does not. The stage field is the most important signal in the entire JSON.

smtp_verified: null is not a boolean

Null is not false. False would mean the server rejected the probe. Null means the probe never happened or never completed. In a bulk run, null appears far more often than false. The reason is greylisting, rate limiting, tarpits, and large providers that simply refuse to chat with probes. A false is actionable. A null is a decision you have to make.

is_greylisted: null is the greylisting story

Greylisting is a deferral tactic. A mail server returns a 4xx "try again later" on the first attempt. A real email service provider retries after a few minutes and the message gets through. A one-shot validation probe gives up and records null. The API cannot know if the address would have accepted a retry. That is why is_greylisted: null and greylisting_note: null appear together. The greylisting rate is the share of rows where the probe quit before the conversation finished.

score: 75 is a warning, not a grade

The score is a composite. With smtp_verified: null, the ceiling is 75. If your threshold is 70, you accept the address. If your threshold is 90, you reject it. The score forces you to own the decision. There is no universal right answer. There is only your risk tolerance.

breach_count: 579 is a trust signal

For test@gmail.com, the breach list is a graveyard of old services. Adobe, Stratfor, Yahoo. The earliest breach is Gawker in 2010. A high breach count does not make an address undeliverable. It makes it less trustworthy for sensitive flows. If you send a password reset to a breached address, you are betting that the current owner is the original owner.

is_free_email and email_provider for segmentation

is_free_email: true with email_provider: "googleworkspace" gives you more than a domain check. You can route Gmail and Outlook differently. You can score B2B leads lower when the provider is a consumer service. Provider ID from MX is harder to spoof than a string match on the domain.

is_role and role_type

is_role: true, role_type: "test". Role addresses are not people. For sales outreach, a role inbox is a dead end. For support tickets, it is exactly what you want. Context matters, and the API gives you the label to apply that context.

Syntax suggestion

The suggestion field is null here because test@gmail.com is spelled correctly. The API can correct gmial.com to gmail.com at signup. That single correction saves more deliverability than a thousand SMTP probes.

i verified 50 emails via smtp. 12 still bounced. what am i missing?

I wrote about that disconnect earlier in i verified 50 emails via smtp. 12 still bounced. what am i missing?. The answer is the same here. SMTP verification is necessary and insufficient. A 250 OK from one probe does not guarantee inbox placement. A null SMTP result does not guarantee failure. The value is in the raw signals, not the final boolean.

i ran 1,000 names through 2 ofac apis. 80 hits disagreed.

The same lesson shows up in sanction screening. In i ran 1,000 names through 2 ofac apis. 80 hits disagreed., the problem was not that one API was wrong. The problem was that each API returned a single verdict while hiding the underlying match strength. Email validation has the same trap. valid: true is a verdict. stage, smtp_verified, is_greylisted, and score are the match strength.

Analysis: forensic honesty and the null problem

The API's honesty creates a product problem. Users want a green check or a red X. The API gives a report card. That tension is the point of forensic honesty. Do not round uncertainty to a boolean. A null is not a no, and a valid: true with stage: "mx" is not a yes.

This pattern shows up outside email validation too. As of 1 September 2026, there are 773 FIPS 140-3 certificates on record. The number validated at Level 4 is zero. Every RFP that says "FIPS 140-3 Level 3 or higher" is technically asking for an empty set. The sentence really means Level 3. That fact becomes urgent on 21 September 2026, when the remaining FIPS 140-2 certificates move to the historical list. Procurement teams will rewrite those sentences. They should rewrite them to say what they actually mean.

Email validation has the same shape. A dashboard that says "verified" implies a range of certainty that may not exist. The valid: true field is the "or higher." The stage and smtp_verified fields are the real level. If you only read the headline, you miss the empty set.

The Oracle layoff story from 14 September 2026 is another mirror. Oracle sent 6 a.m. termination emails to staff. The workforce had already fallen by roughly 21,000 employees, about 13%, from a base of approximately 141,000. The company raised its fiscal 2026 restructuring cost estimate by $700 million, bringing the total to roughly $2.8 billion. The emails were technically delivered. But delivery is not reception. A valid address that reaches a person who was just fired at dawn is still a failure. Validation cannot measure human context.

On 12 September 2026, the validator returned a greylisting note on support@redacted.io. We paused the outbound campaign for 3 hours while two engineers hand-checked 400 addresses. No lesson came out of it. We just lost the time. Nulls cost money even when they are accurate.

SMTP 250 OK is overrated. A single probe that gets a 250 response can still bounce later because the mailbox is full, the user left, or the server accepted the message and then filtered it. Conversely, a null SMTP result from a major provider is often a soft pass, not a reject. The honest signal is the stage and the score, not the boolean.

The is_trusted_identity composite is supposed to mean SMTP verified plus not disposable plus not breached. That composite cannot fire when SMTP is null. So the API pushes you toward a stricter signal, but it does not fake it. That is the design choice I respect.

I'm still not sure if treating null as a soft pass is the right call. Maybe we should retry once and only mark verified if the second attempt completes. But retries make the API slower and can get you blocklisted. There is no clean answer.

12 of 50 emails bounced after smtp 250 ok. do you still trust it?

That question is the heart of the series. In 12 of 50 emails bounced after smtp 250 ok. do you still trust it?, the evidence was that SMTP verification lies by omission. Here, the API reduces the lie by exposing the omission. It does not eliminate it. The SMTP layer is still a snapshot, not a contract.

Implications: what developers should actually do

  • Surface stage to your users. Do not hide it behind a green dot. If validation stopped at MX, say so.
  • Treat smtp_verified: null as unverified, not invalid. Segment those addresses for a retry or a lower confidence score.
  • Use score thresholds that match your risk tolerance. A 75 is not a 95. Do not pretend it is.
  • Check breach_count before sensitive flows. A breached address is a weaker identity anchor even if it is deliverable.
  • Use is_free_email and email_provider for routing. B2B and B2C behavior is different. Provider ID from MX is harder to spoof than domain string matching.
  • Block disposables, but do not block free emails blindly. A Gmail address can be a real customer. A 10-minute mailbox cannot.
  • Respect role and catch-all flags. A role inbox is not a person. A catch-all domain accepts anything.
  • Build retry logic for greylisting. One-shot validation is a guess. Real mail servers retry.
  • Log the raw response, not just the boolean. When a campaign bounces, you will want to know whether SMTP ever completed.

The real implication is that email validation is not a pre-flight check. It is a continuous signal. Validate at signup, re-check before major sends, and re-score after public breach dumps. A single call at registration decays the moment the user changes jobs or the domain changes MX records.

How to use Email Validator API

Here is the same call in Python.

import requests

url = "https://email-validator112.p.rapidapi.com/validate"
params = {"email": "test@gmail.com"}
headers = {
    "x-rapidapi-key": "YOUR_KEY",
    "x-rapidapi-host": "email-validator112.p.rapidapi.com"
}

r = requests.get(url, headers=headers, params=params, timeout=30)
data = r.json()

print("stage:", data["stage"])
print("smtp_verified:", data["smtp_verified"])
print("is_greylisted:", data["is_greylisted"])
print("score:", data["score"])
print("breach_count:", data["deliverability"]["factors"]["breach_count"])
Enter fullscreen mode Exit fullscreen mode

You will need a key from the RapidAPI listing. The docs and issue tracker are on GitHub.

The blind spot I'm still watching

The validator cannot tell you if the human still reads the inbox. It cannot tell you if the address was breached last week in a dump that has not reached HIBP yet. It cannot tell you if a 6 a.m. layoff email just landed in that mailbox. The gap is context. We treat valid: true as permission to send. Sometimes it is only permission to try.

What is the one validation signal you always treat as a yes when it is really just a "not no"?

Part 2 will put a number on the full 10,000-row run: the exact share of valid: true rows that never finished SMTP, and whether waiting a few minutes before retrying turned any of those maybes into real verifications.

Top comments (0)