Verifying a cold list means reading a deliverability verdict for every address and sending only to the safe ones. LeadOcean returns that verdict as email_status on each person it holds. This guide covers the three jobs: size a list, re-check addresses you already own, and export only the rows you can send to.
One limit up front. LeadOcean is a data API, not an upload-and-scrub verifier. It reports status for people in its own database. For an address it does not hold, you need a standalone verifier (see Troubleshooting).
TL;DR
- Time: about 25 minutes for a 200-address test. After that it is one script.
- Cost: sizing a list is free. Each address LeadOcean finds spends 1 record. A miss (404) costs 0. The Free plan has 1,000 records for life.
- Rule: send to
verifiedandcatch_all_valid. Dropinvalid,spam_trap,abuseanddisposable. Treat everything else as a decision, not a default.
Prerequisites
- A LeadOcean API key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. Free needs no card. curland Python 3 withrequests.- Your list as a CSV with an
emailcolumn (for Step 3). - Bulk CSV export (Step 4) needs a paid plan. It is not on Free. Solo is $97 a month, Agency is $497 a month. See pricing.
Step 1: Size the list for free
A count spends no records, so run it before anything else. Send count=true as a query parameter, not in the body, with limit=1. meta.total is capped at 100,000.
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 '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified","catch_all_valid"]}'The data array comes back empty and meta.countOnly is true. The MCP tool leadocean_count_leads runs the same count from Claude or ChatGPT.
Filter values are exact strings. If a filter returns nothing, check the spelling with leadocean_list_enum_values or GET /v1/enums/email_status.
Step 2: Read what each status means
Thirteen statuses exist. Group them before you write any send rule.
| Group | Statuses | Action |
|---|---|---|
| Send | verified, catch_all_valid | Send |
| Decide | catch_all, untested, unknown, risky, role, derived | Verify again elsewhere or send in a small batch |
| Drop | invalid, spam_trap, abuse, disposable, none | Never send |
verified means a deliverability check happened, by LeadOcean's own verifier or a source held to the same standard. catch_all_valid is a mailbox on a catch-all domain whose existence was confirmed through Google Workspace or Microsoft 365. untested means nobody has checked it yet. It may be fine. It is the largest group.
One caveat from the API reference: a people-search row can say verified while enrichment says untested. Treat the search count as an upper bound and trust the enriched answer.
Step 3: Re-check your own addresses by email
Enrich takes exactly one of linkedin_url, email, phone or person_id. Send the email you own with reveal_email and read the status back. A person LeadOcean does not hold is a 404 and costs nothing.
import csv, os, time, requests
URL = "https://api.leadocean.io/v1/people/enrich"
HEAD = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "Content-Type": "application/json"}
out = []
with open("my_list.csv", newline="") as f:
for row in csv.DictReader(f):
r = requests.post(URL, headers=HEAD, json={"email": row["email"], "reveal_email": True})
if r.status_code == 404:
out.append((row["email"], "not_held", ""))
elif r.status_code == 200:
emails = r.json()["data"]["contact_data"]["contact_emails"]
match = [e for e in emails if (e["email"] or "").lower() == row["email"].lower()]
if not match:
out.append((row["email"], "not_held", ""))
continue
e = match[0]
out.append((row["email"], e["status"], e["verified_batch_date"] or ""))
elif r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 1)))
else:
raise SystemExit(f"{r.status_code}: {r.text}")
time.sleep(0.25) # Free is 5 requests a second
with open("checked.csv", "w", newline="") as f:
csv.writer(f).writerows([("email", "status", "verified_batch_date"), *out])Start with 200 rows. That spends at most 200 records. A person held under a different address than yours is written as not_held. A 429 here is simply skipped in this sketch, so retry those rows on a second pass.
Step 4: Export the rows you can send to
To build a fresh list instead of scrubbing an old one, filter on status in the export itself. One request exports up to 50,000 rows, and the account ceiling is 500,000 rows a day.
curl -s -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"vp-sales-ca-sendable","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified","catch_all_valid"]},"limit":1000,"columns":["person_id","profile_full_name","current_company_domain","email_N_address","email_N_status"]}'
curl -s -H "x-api-key: $LEADOCEAN_API_KEY" https://api.leadocean.io/v1/exports/EXPORT_ID
curl -s -H "x-api-key: $LEADOCEAN_API_KEY" -o sendable.csv https://api.leadocean.io/v1/exports/EXPORT_ID/downloadThe create call answers 202 with a job id. Poll until status is done. The CSV headers come back numbered: email_1_address, email_1_status. Every exported row costs one record. If the file holds fewer rows than you asked for, the reserved records are released.
You can also run this from the Exports page in the app: filter builder, column picker and a price preview before you start.
What you get
On 2026-10-21 we ran leadocean_count_leads (free) for VP, Sales & Business Development, Canada, changing only emailStatus:
| emailStatus filter | People |
|---|---|
verified | 1,854 |
catch_all_valid, catch_all | 3,720 |
untested | 7,283 |
invalid | 1,416 |
Each total was exact (totalIsExact: true). These are counts of what each status filter matches, not a mailable list. On the same date catch_all_valid alone matched 0 people in search, so the second row is all catch_all.
The point: on a cold list pulled by title alone, untested is the biggest bucket and invalid is not small. Filtering on status before you export is the cheapest verification step there is.
Step 3 returns 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",
"priority": 1,
"score": null
}
]
}
}
}A verified_batch_date means LeadOcean's own verifier tested that address in that run. A null means it did not run the check itself. It is not a "recently verified" date: only two run dates exist.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong key | Send the key in the x-api-key header. Check $LEADOCEAN_API_KEY is set in this shell. |
| 429 | Above your plan's rate limit (5 requests a second on Free, 200 on Agency) | Wait for the Retry-After header, then retry. Slow the loop or move up a plan. |
| 402 | Records are spent (Free has 1,000 for life) | Move to Solo or Agency. A 402 is not billed. |
| Empty results | A filter value is not an exact enum string | Check it with GET /v1/enums/email_status or leadocean_list_enum_values. |
| 404 on enrich | LeadOcean holds nobody for that email | Free, so no charge. Run those addresses through a standalone verifier such as VerifyHQ. |
| Export refused | Over 50,000 rows a request or 500,000 a day, or you are on Free | Split into several exports, or use a paid plan. |
FAQ
Does LeadOcean verify a list I upload?
No. There is no upload endpoint. You can look up addresses one at a time with POST /v1/people/enrich and read the status for those it holds. For the rest, use a standalone verifier.
Should I send to catch_all addresses?
Only in a small batch first. A catch-all domain accepts any address, so the check cannot confirm the mailbox. catch_all_valid is the exception: that mailbox was confirmed to exist.
How often should I re-check?
Before every send for a cold list. See how often to verify your email list.
Do bounced emails get a refund?
No. There is no credit-back for bounces, so read email_status before you send.
Are there other use cases?
The same filters work for recruiters verifying candidate emails and nonprofits cleaning donor lists. The overview is in the verification and deliverability hub.
Count your sendable list before you send a single email
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →