api, #cybersecurity, #python, #webdev
On October 4, 2026, at 15:11 UTC, I pointed a validator at test@gmail.com. The response came back with a score of 75, MX priorities of 5, 10, 20, 30, and 40, and smtp_verified: null. That null should have been a warning. Two weeks earlier, on September 29, I had run a 50-address campaign through a plain SMTP probe. Every address returned 250 OK. Thirty-eight minutes after send, 12 of them bounced. That's a 24% bounce rate after the server literally said the mailbox was fine.
Here's the call I ran:
curl --request GET \
--url 'https://email-validator112.p.rapidapi.com/email/validate/test%40gmail.com' \
--header 'x-rapidapi-key: $RAPIDAPI_KEY' \
--header 'x-rapidapi-host: email-validator112.p.rapidapi.com'
And the truncated response I got back:
{
"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,
"is_free_email": true,
"email_provider": "googleworkspace",
"is_greylisted": null,
"breach_count": 0,
"breach_status_error": "HIBP_API_KEY invalid or unauthorized",
"is_trusted_identity": null,
"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
}
},
"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"
},
"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}
},
"identity_graph": {
"email": "test@gmail.com",
"breach_count": 0,
"first_breach_date": null,
"last_breach_date": null,
"fetched_at": "2026-10-04T15:11:28.781329+00:00"
},
"fetched_at": "2026-10-04T15:11:28.781372+00:00"
}
A 75 is not a passing grade in my book. Not when I'm about to push a lead list into an ESP. I hard-block anything that scores 75 with smtp_verified: null.
The 38-minute campaign that broke my trust
I need to name the failure first, because this whole article is an autopsy.
On September 29, 2026, I took a list of 50 addresses from an old side-project signup form and ran each one through a Python smtplib handshake. I didn't validate syntax beyond a regex. I didn't check for disposable domains. I didn't look at breach status. I just asked each receiving server, "Hey, is this mailbox real?" and every single one answered 250 OK.
So I shipped the campaign.
Thirty-eight minutes later, my ESP dashboard showed 12 hard bounces. Not soft bounces. Not "mailbox full." Hard bounces. The kind that dent sender reputation. The kind that make your account manager send a polite but firm email. I spent the next four hours scrubbing the list, opening support tickets, and explaining to the team why a "verified" list had cratered. The cost was real: four hours of cleanup, a reputation warning, and a lead-import pipeline nobody trusted for the rest of the quarter.
There is no clean lesson attached. It just happened.
What the API actually returned for test@gmail.com
The response for test@gmail.com is a goldmine of signals: syntax, MX, role type, provider ID, breach status. A raw SMTP probe would have missed them entirely.
First, the score is 75, and the deliverability object repeats the same number, so you can't blame a single field for the bad news. That score is not random. It reflects that syntax and MX are solid, SMTP verification failed, and the address is flagged as a role account. A score of 75 means technically reachable, not a person, and an unconfirmed mailbox. I would not send marketing email to a 75. I hard-block it.
The MX records are real and well-ordered: gmail-smtp-in.l.google.com at priority 5, then alt1 through alt4 at 10, 20, 30, and 40. The mx_found flag is true. The best exchange is gmail-smtp-in.l.google.com. So far, a naive validator would say this email is fine.
But then:
smtp_verified: nullis_role: truerole_type: "test"is_free_email: trueemail_provider: "googleworkspace"is_catch_all: nullis_greylisted: nullbreach_status_error: "HIBP_API_KEY invalid or unauthorized"is_trusted_identity: null
That's a lot of nulls and warnings for an address that passes basic checks.
The role_type: "test" field is especially interesting. test@gmail.com is not a personal inbox. It's a role-style identifier. If you're doing B2B lead scoring, a test role address should be down-weighted or rejected outright. A free-email flag alone doesn't catch that.
Then there's the breach-status error. Because my HIBP key was invalid, the API couldn't check haveibeenpwned.com. The result? breach_status is null, is_trusted_identity is null, and the provenance object still lists breach_status: { "source": "Have I Been Pwned", "confidence": 0.95 } even though the lookup failed. That's the subtle part: the provenance tells you where the signal came from and how confident the system is in the source, not whether the call succeeded.
Look at the provenance object again. It lists smtp_verified: { "source": "SMTP probe", "confidence": 0.9 } even though the top-level smtp_verified is null. The API is telling you it tried the probe and still couldn't confirm the mailbox.
This is the kind of raw signal you can't get from public docs alone.
How to use Email Validator API
If you want to reproduce this yourself, the endpoint is on RapidAPI:
π Email Validator API on RapidAPI
The GitHub repo with examples and issue tracking is here:
π On13uka/email-validator-api on GitHub
A single curl call looks like this:
curl --request GET \
--url 'https://email-validator112.p.rapidapi.com/email/validate/test%40gmail.com' \
--header "x-rapidapi-key: $RAPIDAPI_KEY" \
--header 'x-rapidapi-host: email-validator112.p.rapidapi.com' \
| jq .
For a batch, I use Python and write each response to a JSONL file:
import requests
import json
import csv
import time
url = "https://email-validator112.p.rapidapi.com/email/validate/{email}"
headers = {
"x-rapidapi-key": RAPIDAPI_KEY,
"x-rapidapi-host": "email-validator112.p.rapidapi.com"
}
with open("emails.csv") as f, open("results.jsonl", "w") as out:
reader = csv.reader(f)
for row in reader:
email = row[0]
r = requests.get(url.format(email=email), headers=headers)
out.write(json.dumps(r.json()) + "\n")
time.sleep(0.2) # be polite
I aggregate with jq:
jq -s 'map(select(.smtp_verified == null)) | length' results.jsonl
jq -s 'map(select(.is_disposable == true)) | length' results.jsonl
jq -s 'map(select(.is_trusted_identity == null)) | length' results.jsonl
The repo has more complete examples, including error handling for the HIBP key failure and greylisting retries.
Why SMTP 250 OK is not a trust score
I used to think SMTP verification was the final word. It's not. It's one signal, and it's a noisy one.
An SMTP 250 OK only means the receiving server accepted your RCPT TO command at that exact moment. It does not mean:
- the mailbox belongs to a real person
- the domain isn't a catch-all that accepts everything
- the server won't greylist or rate-limit you on the real send
- the address hasn't been burned in a breach
- the local part isn't a role alias like
test,support, oradmin
In other words, SMTP 250 OK is confident and often wrong. That pattern has a name. In a Synthpop.ai study published October 1, 2026, researchers ran half a million API calls against Jev, a decision model from TypeSafe AI that promises "calibrated decisions" with "epistemically honest probabilities." The study found that a model is either wrong and unsure, or wrong and so confident that nobody checks. The confidence number becomes the gatekeeper, and if it doesn't drop when the model is guessing, the mistake goes straight through.
That's exactly what SMTP 250 OK did to my campaign: it acted as a confidence gate that said yes to all 50 addresses, and twelve of those yeses turned out to be lies. I now trust a null smtp_verified more than a polite 250 OK.
F-Secure's July 6, 2026 research on AI shopping agents makes a related point: when an automated system acts on your behalf, trust has to be earned through verification, not assumed from a single green light. An AI agent that buys a coat without checking the seller is the same shape of bug as an email pipeline that sends on 250 OK without checking identity signals.
My position now is blunt: SMTP 250 OK is overrated as a deliverability signal. I won't ship a campaign on it alone again.
This is also why I keep coming back to the related experiment where i verified 5,000 emails. smtp said ok, 24% still bounced. The 24% figure isn't a fluke. It's a pattern.
The breach-status gap that is_trusted_identity couldn't close
The API exposes a composite field called is_trusted_identity. It's supposed to be green only when SMTP is verified, the address is not disposable, and the breach status is clean. It's the closest thing to a single "should I trust this email?" signal.
But in my test@gmail.com call, is_trusted_identity was null. Not false. Null.
Why? Because breach_status_error said "HIBP_API_KEY invalid or unauthorized". The breach check never ran, so the composite couldn't compute. The identity graph still shows breach_count: 0, but the provenance makes it clear the HIBP lookup failed. If I had blindly trusted breach_count: 0, I would have assumed the address was clean when the API was actually telling me it didn't know.
This is a real edge case you only see in production. An expired or unauthorized HIBP key doesn't throw a 500. It returns a successful JSON payload with a quiet error nested three levels deep. If your gating logic treats null as truthy, or treats breach_count: 0 as "no breaches," you just let an unverified address through.
This is why the breach status signal matters as its own line item, not just as an input to is_trusted_identity. The composite is useful, but it's fragile. When one upstream source fails, the whole composite collapses to null, and null is a dangerous default.
If you want to see how messy breach data can get at scale, read i tested 500 emails against hibp. my validator found a major flaw.
Free emails, role addresses, and the provider ID surprise
Another thing the raw SMTP probe missed: test@gmail.com is flagged as both a free email and a role address.
is_free_email: trueemail_provider: "googleworkspace"is_role: truerole_type: "test"
The provider ID is more specific than I expected. It doesn't just say "Gmail." It says googleworkspace. That matters if you're segmenting leads into B2B vs B2C. A signup with a googleworkspace address is either a small business using Google Workspace or a consumer Gmail account the API mapped to the Workspace infrastructure. Either way, it's a signal you use.
The is_role flag is even more actionable. test as a role type is a dead giveaway that this is not a real user. If your onboarding flow sends a welcome sequence to test@anything, you're probably talking to a QA environment, not a customer.
A plain SMTP probe would have said "yes, this mailbox exists." The API says "yes, it exists, but it's a role account on a free provider and we couldn't verify the mailbox."
Greylisting and catch-all: the silent killers
Two more fields came back null: is_catch_all and is_greylisted.
smtp_verified: null happens for a few reasons. The server refused the SMTP probe. The server greylisted the connection, temporarily rejecting unknown senders to discourage spam. Or the domain is catch-all, accepting every local part and making 250 OK meaningless.
A catch-all domain is a deliverability trap. You think you're validating, but the server is just being polite. You send to not-a-real-user@example.com, it says 250 OK, and then the message disappears into a black hole or bounces later. That's worse than a hard bounce at validation time, because you paid to send it.
Greylisting is sneakier. The first SMTP probe gets a temporary failure. If your validator doesn't retry, it records the address as unverified. If your ESP retries on the real send, the message goes through. So smtp_verified: null isn't always a "bad" address; it is an address that needs patience.
I ran into this in another experiment where i validated 10,000 emails. the greylisting rate shocked me. I treat every null as a question, not an answer.
What I changed in our pipeline
After the 24% bounce, I rewrote our email gate. Here's what it looks like now:
- Never send on SMTP 250 OK alone. SMTP verification is necessary but not sufficient.
-
Require
smtp_verified: true. If it's null, the address goes into a retry queue or a manual-review bucket. -
Reject
is_disposable: true. Disposable domains have gotten good at looking legitimate, so MX checks aren't enough. -
Treat
is_catch_all: trueas suspicious. It accepts mail, but verification is impossible. -
Down-weight or reject role addresses for personal-account flows.
test,admin,support,noreplyβthese are not leads. -
Use
is_free_emailandemail_providerfor B2B vs B2C segmentation, not for blocking. -
Handle
is_trusted_identity: nullexplicitly. If the composite is null because of a breach-status error, don't treat it as a green light. Either fix the HIBP key or fall back to stricter checks. -
Surface syntax suggestions. A typo like
gmial.comshould be corrected before it ever hits the validator. - Log provenance. The confidence scores and source names are useful for debugging why a decision was made.
The goal isn't to block every risky email; it's to know what you're risking. A 75-score role address on a free provider with no SMTP verification is a very different bet than a 95-score personal address with clean breach history.
The question I'm still debating
I'm still not sure where to draw the line on the HIBP failure case. If is_trusted_identity is null because the breach lookup failed, should I block the signup or let it through? Blocking means I'm rejecting users because a third-party key expired. Letting it through means I'm accepting the bounce and breach risk.
So here's the specific stack choice I'd debate with anyone building the same pipeline: Would you rather fail closed on is_trusted_identity: null and absorb the support tickets from false rejects, or fail open and absorb the bounce risk from unverified identities? Why?
Give me the raw signals every time. I'd rather debug a noisy API response than explain a 24% bounce rate to my ESP.
Top comments (0)