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

How-to guide

How to Test an Email Address Without Sending an Email (Four Checks)

Four checks in order of cost: syntax, MX record, SMTP probe, and a lookup of the address status in LeadOcean.

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

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 status on record for that address. It spends 1 record when the address is found and 0 when it is not.
  • Treat catch_all, untested and unknown as "not proven", not as "good".

Prerequisites

  • A terminal with dig and openssl (or nc).
  • 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.

python
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"))       # False

This 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.

bash
dig +short MX gmail.com | sort -n

Output from a run on 2026-10-21 (first three lines):

code
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).

bash
nc mx1.acme.com 25

Use 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:

code
HELO check.example.org
MAIL FROM:<check@example.org>
RCPT TO:<jane.doe@acme.com>
QUIT

Read the reply to RCPT TO:

ReplyMeaning
250Server accepts the recipient.
550Mailbox does not exist.
4xxTry 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.

bash
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:

bash
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:

code
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 email

A lookup response has this shape (response shape from the API reference, placeholder values):

json
{
  "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: verified and catch_all_valid.
  • Do not send: invalid, spam_trap, abuse, disposable.
  • Use with care: risky, role (a shared mailbox such as info@) and derived.
  • Not proven: catch_all, untested, unknown. untested means 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

ErrorCauseFix
401Missing or wrong x-api-key header.Check $LEADOCEAN_API_KEY is exported and sent as x-api-key.
404 on enrichLeadOcean holds nothing for that address. Costs 0 records.Run steps 1 to 3 on it instead.
429You went above your plan's rate limit (5 a second on Free, 200 on Agency).Honor the Retry-After header and slow the loop.
402The 1,000 free records are spent.Check GET /v1/account, or move to Solo ($97 a month) or Agency ($497 a month).
Empty search resultsA filter value is not an exact enum string.List valid values with leadocean_list_enum_values or GET /v1/enums.
nc hangs on port 25Your 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 →