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

How-to guide

How to Verify a Cold Email List in Bulk (Using the LeadOcean API)

Size the list for free, read the status on every address, drop the ones that bounce, and export the rest. Real queries and real output.

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

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 verified and catch_all_valid. Drop invalid, spam_trap, abuse and disposable. 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.
  • curl and Python 3 with requests.
  • Your list as a CSV with an email column (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.

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 '{"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.

GroupStatusesAction
Sendverified, catch_all_validSend
Decidecatch_all, untested, unknown, risky, role, derivedVerify again elsewhere or send in a small batch
Dropinvalid, spam_trap, abuse, disposable, noneNever 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.

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

bash
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/download

The 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 filterPeople
verified1,854
catch_all_valid, catch_all3,720
untested7,283
invalid1,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):

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",
          "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

ErrorCauseFix
401Missing or wrong keySend the key in the x-api-key header. Check $LEADOCEAN_API_KEY is set in this shell.
429Above 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.
402Records are spent (Free has 1,000 for life)Move to Solo or Agency. A 402 is not billed.
Empty resultsA filter value is not an exact enum stringCheck it with GET /v1/enums/email_status or leadocean_list_enum_values.
404 on enrichLeadOcean holds nobody for that emailFree, so no charge. Run those addresses through a standalone verifier such as VerifyHQ.
Export refusedOver 50,000 rows a request or 500,000 a day, or you are on FreeSplit 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?

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 →