Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Build a Super Email Verifier (Four Layers, Real Code)

A super email verifier is not one tool. It is four checks in a fixed order, and a rule for what to do with each answer.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

A "super" email verifier is a pipeline, not a product: cheap local checks first, a mailbox probe second, a stored verdict third, and a send rule at the end. Each layer removes a different kind of bad address.

Most guides on this keyword explain why verification matters. This one gives you the code and the decision table. For tool comparisons, read best email verification tools. For the concepts, read email verification explained.

TL;DR

  • Run four layers in order: syntax, DNS, SMTP, then a stored status. Layers 1 to 3 cost nothing and take about 20 minutes to wire up.
  • The hard part is the answer "maybe". Catch-all domains return it for every address, so the pipeline needs a rule for it, not a better probe.
  • Sizing a list by emailStatus on LeadOcean spends 0 records. Pulling or enriching a person spends 1.

Prerequisites

  • Python 3.10 or newer and dig on your path.
  • Outbound port 25 for layer 3. Many cloud hosts block it.
  • A LeadOcean key in $LEADOCEAN_API_KEY for layer 4. The free plan has 1,000 records for life and no card. The free MCP tool leadocean_count_leads also works for sizing.

Step 1: Reject what cannot be an address

Syntax is the cheapest layer, so it runs first and stays permissive. Reject only what no mail server would accept.

python
import re

SYNTAX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]{2,}$")

def layer1(addr: str) -> str:
    if len(addr) > 254 or not SYNTAX.match(addr):
        return "invalid"
    return "pass"

Do not write a clever regex. A strict one rejects real addresses, and a pass proves nothing about the mailbox.

Step 2: Check that the domain can receive mail

If the domain has no mail server, nothing you send can arrive. The check is a DNS query.

python
import subprocess

def dig(kind: str, domain: str) -> list[str]:
    out = subprocess.run(["dig", "+short", kind, domain],
                         capture_output=True, text=True).stdout
    return [l.split() for l in out.strip().splitlines() if l.strip()]

def layer2(addr: str):
    domain = addr.split("@", 1)[1]
    mx = sorted(dig("MX", domain), key=lambda r: int(r[0]))
    if mx:
        return "pass", mx[0][1].rstrip(".")
    if dig("A", domain):          # RFC 5321: no MX falls back to the A record
        return "pass", domain
    return "invalid", None

RFC 5321 (checked September 2026) is why the fallback to the address record exists. Returning the host saves a second lookup in step 3.

Step 3: Ask the mail server, and accept "maybe"

The SMTP probe asks whether the server would take mail for the address, then quits before any message is sent. It is the only direct test of one mailbox, and the least reliable layer.

python
import smtplib

def layer3(addr: str, host: str) -> str:
    try:
        with smtplib.SMTP(host, 25, timeout=10) as s:
            s.helo("probe.example.org")
            s.mail("check@example.org")
            code, _ = s.rcpt(addr)
            junk, _ = s.rcpt("zz-no-such-user-91@" + addr.split("@")[1])
    except (OSError, smtplib.SMTPException):
        return "unknown"
    if code == 250 and junk == 250:
        return "catch_all"      # server accepts anything: 250 proves nothing
    if code == 250:
        return "pass"
    if code in (550, 551, 553):
        return "invalid"
    return "unknown"            # 4xx, greylisting, blocked port

The second RCPT TO is the useful trick. A server that accepts a made-up mailbox is a catch-all, so its yes for your address means nothing. Large providers also rate-limit probes, and probing from a shared IP can get it blocklisted. Run this layer from a dedicated host at a low rate, or skip it and use step 4.

Step 4: Read a stored verdict

A stored status is a verdict someone collected in bulk, so you skip steps 2 and 3 for every address in the database. LeadOcean publishes one emailStatus per person. The values are verified, catch_all_valid, catch_all, risky, unknown, untested, invalid, role, disposable, spam_trap, abuse, derived and none.

Size first. The count is free. This is the body for leadocean_count_leads, the MCP tool, with emailStatus set to one value at a time:

json
{
  "country": ["DE"],
  "jobLevel": ["C-Team"],
  "emailStatus": ["verified"],
  "facets": false
}

Then pull rows over REST. The body is flat JSON, filters are arrays and limit runs from 1 to 100.

bash
curl -s https://api.leadocean.io/v1/people/search \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"country":["DE"],"jobLevel":["C-Team"],
       "emailStatus":["verified"],"limit":25}'

To check one person you already hold, enrich by linkedin_url, email, phone or person_id, with reveal_email on.

bash
curl -s https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane.doe@acme.com","reveal_email":true}'

Two rules from the API reference (checked October 2026) change how you read the answer. On a business address, verified means LeadOcean's own verifier tested it. On a personal address it only means a supplier asserted it. And a verified count at search time is an upper bound, because enrichment applies the stricter per-address rule.

LeadOcean does not verify an arbitrary address you paste in. It returns the status of addresses it holds. For one-off lists from elsewhere, keep layers 1 to 3 in front of your own data.

Step 5: Turn each status into a send rule

The pipeline only helps if every answer maps to an action. This table is the part to bookmark.

StatusMeaningAction
verifiedTested and acceptedSend
catch_all_validDomain accepts anything, mailbox confirmed through a Microsoft 365 or Google Workspace checkSend
catch_allDomain accepts anything, mailbox unconfirmedSend in a small, separate batch. Watch bounces.
risky, role, derivedWeak signal, shared inbox or pattern guessHold. Do not send cold.
untested, unknownNobody checked it, or the check was inconclusiveRun layers 2 and 3 yourself first
invalid, disposable, spam_trap, abuseKnown badDrop
noneNo email heldSkip
python
SEND = {"verified", "catch_all_valid"}
TRIAL = {"catch_all"}
DROP = {"invalid", "disposable", "spam_trap", "abuse", "none"}

def route(status: str) -> str:
    if status in SEND:  return "send"
    if status in TRIAL: return "trial_batch"
    if status in DROP:  return "drop"
    return "recheck"

LeadOcean gives no refund or credit-back for bounces, so this routing is your protection. Read the status before you send.

What you get

These counts come from leadocean_count_leads on 2026-10-21. The filter is country DE, job level C-Team, plus the single emailStatus shown. All four calls spent 0 records.

emailStatusPeopleDistinct companies
verified69,14942,748 (estimate)
catch_all56,02128,407
risky6,7405,850
untested150,02583,353 (estimate)

As a filter, catch_all also matches catch_all_valid, so that row includes both. The largest group is untested, more than double verified. That is the real shape of any email database: most addresses have simply never been checked. The verified figure is an upper bound at search time.

Enrichment returns a small email object. This is the response shape from the API reference, with placeholder values:

json
{
  "email": "jane.doe@acme.com",
  "type": "work",
  "status": "verified",
  "verified_batch_date": "2026-09-20"
}

A non-null verified_batch_date means LeadOcean's own verifier tested the address in that run. A null means the status came from a source held to the same standard, not a LeadOcean run. A whole batch shares one date, so it is not a freshness timestamp.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY is exported and the key is active in the app.
429Above the plan's rate limit (5 requests a second on Free, 200 on Agency)Wait for the Retry-After value, then retry.
402The 1,000 free records are spentCheck GET /v1/account, or move to a paid plan.
Empty resultsA bad enum value, such as "VP Sales" for jobLevelUse exact strings: C-Team, VP, Director, Manager, Staff, Other. List values with leadocean_list_enum_values.
Export refusedOver 50,000 rows in one request or 500,000 in a daySplit into several POST /v1/exports calls with narrower filters. Bulk CSV export is not on Free.
SMTP timeout (step 3)Port 25 blocked by your networkRun the probe from a host that allows outbound 25, or rely on step 4.

FAQ

What makes a verifier "super"?

Layers and rules, not a bigger probe. Local checks remove typos and dead domains, SMTP tests the mailbox, a stored status covers what SMTP cannot, and a routing table decides what to send.

Can any verifier confirm a catch-all address?

Not by probing, because the server says yes to everything. LeadOcean marks a mailbox catch_all_valid when a Microsoft 365 or Google Workspace identity check confirmed it. Everything else on those domains stays catch_all.

Should I verify before or after I buy a list?

Before. Size by emailStatus first, which is free, and pull only the statuses your send rule accepts. See the email verifiers benchmark for how verifiers compare.

How many records does this pipeline use?

Layers 1 to 3 use none, and neither does a count. Each person you pull or enrich costs 1 record. An enrichment that finds nobody is a 404 and costs nothing.

Filter by email status before you send

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →

Back to the blog hub.