TL;DR
- Accurate prospecting data is a pipeline, not a vendor claim: size the list, filter on verification, confirm each address on enrichment, then gate on status before you send.
- It takes about 20 minutes and spends 20 LeadOcean records. Counts are free.
- The Free plan (1,000 records for life, no card) covers it many times over.
Prerequisites
- A LeadOcean API key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. Free gives you 1 key and 5 requests a second. curlandjq.- Optional: the hosted MCP server at
https://api.leadocean.io/mcp(13 tools) if you would rather size the list from an AI client.
Step 1: Size the audience before you spend anything
Count first. A free count tells you whether the filter is empty, too broad or worth pulling.
Send count=true as a query parameter, not in the body, with limit 1. The body rejects unknown fields.
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["201-500", "501-1000"],
"limit": 1
}' | jq '.meta | {total, totalIsExact, distinctCompanies}'meta.total is capped at 100,000. meta.distinctCompanies tells you how many employers the people span, which is the number to compare against your account list.
Enum values are exact strings. Check them with GET /v1/enums/job_level instead of guessing.
Step 2: Filter on email status, and know what the filter means
Pick the status filter on purpose. The default mailable audience is verified, catch_all_valid and catch_all. Narrow to verified when the cost of a bounce is high.
Run the same filter both ways with the MCP tool leadocean_count_leads (free). On 2026-10-21 it returned this:
| Filter (VP, Sales & Business Development, US, 201 to 1000 employees) | Total | Distinct companies |
|---|---|---|
| Default mailable (verified, catch_all_valid, catch_all) | 20,881 | 9,764 |
emailStatus: ["verified"] only | 6,375 | 3,572 |
| No email condition at all | 55,751 | not returned |
verified on a business address means LeadOcean's own verifier sent to the address and it accepted. A supplier saying an address is good does not earn the word. Those addresses are published as untested.
One caveat matters. People search carries no per-address verification provenance, so a search row can say verified and enrichment can say untested. Treat a search count of verified as an upper bound. The enriched answer is the correct one.
Step 3: Pull a page and confirm each address on enrichment
Pull a small page with the verified filter. Each person returned is one record.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["201-500", "501-1000"],
"emailStatus": ["verified"],
"limit": 10
}' > page1.jsonSearch rows are a thin projection with no contact points. Each row carries its id at meta.source_ids.person_id. List them, then enrich each one with reveal_email true.
jq -r '.data[].meta.source_ids.person_id' page1.jsonEnrich takes exactly one of linkedin_url, email, phone or person_id.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "person_id": "PERSON_ID_FROM_PAGE1", "reveal_email": true }'Page with meta.nextCursor, passed back unchanged. One search stops at 10,000 rows (meta.depthCapped true). Narrow the filters instead of walking further.
Step 4: Gate on status and date before anything is sent
Keep a row only if the enriched status passes. The address object carries status and verified_batch_date. The date is the verification run, not a timestamp for the address, and it can be null.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "person_id": "PERSON_ID_FROM_PAGE1", "reveal_email": true }' \
| jq '.data.contact_data.contact_emails[0]
| select(.status == "verified" or .status == "catch_all_valid")
| {email, status, verified_batch_date}'Rules worth encoding:
- Send:
verified,catch_all_valid. - Send with care:
catch_all. The domain accepts everything and the mailbox cannot be confirmed. - Hold:
risky,role,derived,untested,unknown. - Drop:
invalid,spam_trap,abuse,disposable.
Bounced emails are not refunded or credited back. Read email_status before sending, not after.
Step 5: Cross-check the account side and the role
Accurate contacts at the wrong accounts still waste a sequence. Size the company list for free with GET /v1/companies/count. It takes the company-search filters and returns an exact total with breakdowns by industry, headcount band, HQ country, HQ state and NAICS.
curl -s "https://api.leadocean.io/v1/companies/count?industry=Computer%20Software&employeeRange=201-500" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq '.data | {total, totalIsExact}'Compare that total with meta.distinctCompanies from Step 1. If the people span far fewer employers than the company list holds, your role filter is too narrow for the account list. If they span more, the title filter is pulling adjacent teams.
Enrichment also returns career history in resume_data: past roles with title, company name, dates and job level, plus education and certifications when held. Use it to catch a stale title. A person whose current role started last month and whose previous role matches your ICP is a better target than the row alone suggests. Past employers carry no company_id yet, so match them by name.
The same Step 1 and Step 2 counts work from an AI client. Ask the MCP tool leadocean_count_leads for the numbers, and spend records only when you move to Step 3.
A status check you can run on your own sample
Do not take any vendor's accuracy number on trust, ours included. Enrich 10 rows, save each response as enriched_1.json to enriched_10.json, send nothing, and tally the statuses.
jq -r '.data.contact_data.contact_emails[0].status' enriched_*.json | sort | uniq -cIf the verified filter returned 10 rows and enrichment confirms 6, your search-level count overstates the safe list by about 40 percent for that segment. Apply that ratio to the Step 2 total before you size a campaign. Repeat per segment, because the gap differs by company size and region. Ten records is a cheap test.
Why this order
Each step removes a different kind of error. Counts catch an empty or runaway filter before it costs anything. The status filter removes addresses nobody has tested. Enrichment replaces the search-level label with the per-address verdict. The gate keeps risky mail out of the sequencer. The account cross-check keeps good contacts at the right companies.
Skip a step and the error moves downstream, where it costs more. A bounce is paid for in sender reputation, and nothing is credited back.
What you get
This is the response shape from the API reference, with placeholder values. It is not a real record.
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": {
"has_email": true,
"has_phone": false,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{
"email": "jane.doe@acme.com",
"type": "work",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
],
"contact_current_experiences": [
{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com", "start_date": "2025-06-01" }
]
},
"resume_data": {
"experiences": [{ "job_title": "Director of Sales", "company_name": "Globex", "start_date": "2021-01-01", "end_date": "2025-05-31" }],
"educations": []
},
"meta": { "source_ids": { "person_id": "123456789" }, "fetched_at": "2026-10-01" }
},
"meta": { "credits": 1 }
}The sizing numbers in Step 2 are real, taken from leadocean_count_leads on 2026-10-21. A 6,375-person verified audience across 3,572 companies is a list you can work.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or bad x-api-key. | Re-export $LEADOCEAN_API_KEY and check there is no trailing space. |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency). | Honor Retry-After, or move to a higher plan. |
| 402 | Out of records. Free is 1,000 for life. | Check GET /v1/account, then move to Solo ($97 a month) or Agency ($497 a month). |
Empty data, no error | An enum value that does not exist, such as "Vp". | List the exact strings with GET /v1/enums/job_level. |
| 404 on enrich | Nobody matched that person_id. | It costs nothing. Drop the row. |
meta.depthCapped true, no nextCursor | One search reached 10,000 rows. | Split the filter (by country or employee range) and run several searches. |
FAQ
Is a search count of verified a promise of deliverable mail?
No. It is an upper bound. Search has no per-address provenance, so confirm each address on enrichment and use that status.
Why do the default count and the verified count differ so much?
The default includes catch_all and catch_all_valid. On 2026-10-21 the same filter gave 20,881 mailable and 6,375 verified. Choose by how much a bounce costs you.
How fresh is the data?
The dataset is refreshed monthly, and each record carries its own fetched_at date. Read it, and read verified_batch_date on the address.
Does the count spend records?
No. Counts, filter lists, enum lookups and an enrichment that finds nothing are free.
Which plan fits this?
Free covers the recipe. Solo at $97 a month suits one person pulling lists. Agency at $497 a month suits teams. See pricing.
Related reading: how accurate is B2B data, sales intelligence and B2B prospecting platforms, AI tools for sales prospecting.
Check your list before you send it
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →