You test an address without sending by running four checks in order: syntax, MX record, SMTP probe, then a status lookup. Each one rules out a different failure. None of them alone proves a person reads that mailbox.
TL;DR
- Run syntax, MX and SMTP checks yourself in about 10 minutes, for free. Outbound port 25 is often blocked on laptops and cloud hosts.
- A LeadOcean lookup by email returns the
statuson record for that address. It spends 1 record when the address is found and 0 when it is not. - Treat
catch_all,untestedandunknownas "not proven", not as "good".
Prerequisites
- A terminal with
digandopenssl(ornc). - A LeadOcean API key, stored as
$LEADOCEAN_API_KEY. The free plan has 1,000 records for life, no card, and 5 requests a second. - Placeholder address used below:
jane.doe@acme.com. Swap in your own.
Step 1: Check the syntax
A syntax check catches typos and missing parts before you spend a network call. It cannot tell you whether the mailbox exists.
import re
PATTERN = re.compile(
r"^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]{1,64}"
r"@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?"
r"(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$"
)
def syntax_ok(email: str) -> bool:
return bool(PATTERN.match(email)) and len(email) <= 254
print(syntax_ok("jane.doe@acme.com")) # True
print(syntax_ok("jane.doe@acme")) # FalseThis is a pragmatic pattern, not a full parser for every legal address. For the rules, read what email address syntax is.
Step 2: Look up the MX record
If the domain has no MX record, mail has nowhere to go. One command answers it.
dig +short MX gmail.com | sort -nOutput from a run on 2026-10-21 (first three lines):
5 gmail-smtp-in.l.google.com.
10 alt1.gmail-smtp-in.l.google.com.
20 alt2.gmail-smtp-in.l.google.com.An empty result means the domain cannot receive mail. A domain that returns nothing is a hard stop. Lowest number wins, so probe the host with the lowest priority first.
Step 3: Probe the mail server with SMTP
An SMTP probe opens a conversation with the receiving server and quits before the message body. It follows the commands in RFC 5321 (September 2026).
nc mx1.acme.com 25Use the lowest-priority MX host of the address's own domain from step 2 (mx1.acme.com is a placeholder).
Then type these lines, one at a time:
HELO check.example.org
MAIL FROM:<check@example.org>
RCPT TO:<jane.doe@acme.com>
QUITRead the reply to RCPT TO:
| Reply | Meaning |
|---|---|
| 250 | Server accepts the recipient. |
| 550 | Mailbox does not exist. |
| 4xx | Try later. Treat as unknown. |
Three limits to know. Many providers block port 25 from home and cloud networks, so the connection hangs. Some servers answer 250 for every address (a catch-all). Some throttle or blacklist IPs that probe often, which hurts your own sending reputation if you probe from your mail IP. Read what SMTP verification is before you run this at volume.
Step 4: Look up the status in LeadOcean
LeadOcean does not run a live probe when you call it. It looks the address up in its people data and returns the status on record. That makes it fast and safe at volume, and it means an address we hold nothing for returns a 404 and costs nothing.
curl -s -X POST 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}'Send exactly one identifier per call. For a list, loop with a pause that keeps you under your plan's rate limit (5 requests a second on Free, 200 on Agency).
If you have no address yet, search first. Sizing a search is free:
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emailStatus": ["verified"], "jobLevel": ["VP"], "country": ["US"]}'In Claude or another MCP client, the free leadocean_list_enum_values tool lists the status values, so you never guess a string.
What you get
The email status values the API accepts, from leadocean_list_enum_values on 2026-10-21:
verified Deliverable
catch_all_valid Catch-all, mailbox confirmed
catch_all Catch-all domain
risky Risky
unknown Unknown
untested Not yet checked
invalid Undeliverable
role Role address
disposable Disposable address
spam_trap Spam trap
abuse Abuse / complainer
derived Derived, not verified
none No emailA lookup response has this shape (response shape from the API reference, placeholder values):
{
"success": true,
"data": {
"contact_data": {
"has_email": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{
"email": "jane.doe@acme.com",
"type": "work",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
]
}
}
}How to act on it:
- Send:
verifiedandcatch_all_valid. - Do not send:
invalid,spam_trap,abuse,disposable. - Use with care:
risky,role(a shared mailbox such as info@) andderived. - Not proven:
catch_all,untested,unknown.untestedmeans nobody has checked the address yet. It may be fine.
verified_batch_date is the date of the verification run. A null date means the check was not run by LeadOcean. Personal addresses are never tested by LeadOcean, so treat verified on a personal address as a lead to verify.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header. | Check $LEADOCEAN_API_KEY is exported and sent as x-api-key. |
| 404 on enrich | LeadOcean holds nothing for that address. Costs 0 records. | Run steps 1 to 3 on it instead. |
| 429 | You went above your plan's rate limit (5 a second on Free, 200 on Agency). | Honor the Retry-After header and slow the loop. |
| 402 | The 1,000 free records are spent. | Check GET /v1/account, or move to Solo ($97 a month) or Agency ($497 a month). |
| Empty search results | A filter value is not an exact enum string. | List valid values with leadocean_list_enum_values or GET /v1/enums. |
nc hangs on port 25 | Your network blocks outbound SMTP. | Run the probe from a host that allows port 25, or skip to step 4. |
FAQ
Can I be sure an address is real without sending?
No. Only a reply or a delivered message proves a person reads it. A passed SMTP probe shows the server accepted the recipient at that moment.
Why does a catch-all domain break the SMTP probe?
A catch-all server accepts every recipient and bounces later. To detect one, probe a random address at the same domain. If it is also accepted, a 250 for your address proves nothing. LeadOcean's catch_all_valid status marks a mailbox confirmed on a catch-all domain.
Does a status lookup count against my records?
An enrichment that finds the person costs 1 record. A lookup that finds nothing, and a sized search with count=true, cost nothing. See pricing.
Should I check disposable addresses separately?
Yes. A disposable email address passes syntax, MX and often SMTP checks. LeadOcean flags them as disposable. Short-lived inboxes are covered under temporary email address.
More recipes like this are on the LeadOcean blog.
Check your next list before it bounces. Get a LeadOcean key.
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →