A list converts for two reasons: the person reading it owns the problem, and the message reaches an inbox. Everything else is polish. This guide builds a list around those two tests with real requests against the LeadOcean API.
Lists fail in predictable ways. The audience is too wide, so the message fits no one. The addresses are unchecked, so mail bounces and the sending domain pays. The file is old, so part of it points at people who changed jobs.
Most list-building advice stops at "define your audience" and "keep it clean". Here each of those is a query. You size the audience for free, filter by role and company size, and refuse any address that is not mailable.
The example target is VPs of Sales in Canada. Swap the filters for your own market.
TL;DR
- It takes about 25 minutes to size an audience, pull a sample, reveal one address and export a small list.
- The walkthrough spends about 31 records: 5 from the sample search, 1 from the reveal and 25 from the export. Sizing and counts are free. CSV export needs Solo or above.
- Mail only
verified,catch_all_validorcatch_all. Every filter you add makes the list smaller and the audience tighter, so size before you spend.
Prerequisites
- A LeadOcean API key from app.leadocean.io. The free plan has 1,000 records, one-off, and no card.
- The key in your shell as
$LEADOCEAN_API_KEY. curl,jqand Miller (mlr) for the cleanup in Step 6.- A way to send mail. Any sequencer works. This guide stops at the CSV.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Define the buyer as filters, not adjectives
Write the buyer as filter values before you open any tool. A filter is a test a person passes or fails. "Senior sales leader at a mid-size company" is not.
{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"employeeRange": ["51-200", "201-500"]
}Enum values are exact strings. VP works and Vice President returns nothing. Check a value first with the free leadocean_list_enum_values MCP tool, or GET /v1/enums.
Write down the one sentence your email will open with. If a filter does not help that sentence stay true for every row, drop it. If the sentence needs a filter you lack, such as a tool the company uses, find the matching one with GET /v1/filters or leadocean_list_filters. Both are free.
Three filters do most of the work: role (jobLevel plus jobFunction), geography (country or salesRegion) and company size (employeeRange). Add industry or technologyCategories only when your offer is specific to one.
Step 2: Size the list before you spend a record
Count how many people match, for free. Send count=true and limit=1 as query parameters. Arrays go comma-separated.
curl -s -G https://api.leadocean.io/v1/people/search \
-H "x-api-key: $LEADOCEAN_API_KEY" \
--data-urlencode "jobLevel=VP" \
--data-urlencode "jobFunction=Sales & Business Development" \
--data-urlencode "country=CA" \
--data-urlencode "emailStatus=verified,catch_all_valid,catch_all" \
--data-urlencode "count=true" \
--data-urlencode "limit=1" | jq '.meta | {total, totalIsExact, countOnly}'The call returns no rows and spends nothing, so run it as often as you like. Run it with and without each filter to see which one cuts the list most. A list under about 500 is a hand-written campaign. A list over 100,000 is too broad to convert, because meta.total stops at 100,000 and the filters are not doing any work yet.
Through MCP, the same call is leadocean_count_leads. It also defaults to mailable addresses, which is the number you care about.
Step 3: Pull a sample and read it
Read five real rows before you commit to the list. Each person returned costs 1 record, so keep limit small.
curl -s 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": ["CA"],
"employeeRange": ["51-200", "201-500"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"],
"limit": 5
}' | jq '.data[] | {person_id: .meta.source_ids.person_id, contact_data}'Read the titles and companies. If two of five are the wrong person, tighten title with an exact match in brackets, such as "[VP Sales]", or add excludeTitle. The check is the point of this step. You spend 5 records to learn whether the filters describe the right people, before you spend 25 or 25,000.
Search returns a thin row. It carries a person_id and flags such as has_email, not the address.
Step 4: Reveal only the addresses you will send
Send the person_id to enrich with reveal_email set to true. Enrich takes exactly one of linkedin_url, email, phone or person_id.
curl -s 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_STEP_3", "reveal_email": true}' \
| jq '.data.contact_data | {email_status, contact_emails}'This costs 1 record. A person we hold nothing for is a 404 and costs nothing.
The status is the gate. verified was tested. catch_all_valid means the mailbox was confirmed on a domain that accepts everything. catch_all means the domain accepts everything and the mailbox is unproven. Everything else, including risky, unknown and role, stays out. LeadOcean gives no refund for bounced emails, so read email_status before you send.
Step 5: Export the list as a CSV
Export the whole audience in one request. Bulk CSV export is not on Free: it is included on Solo ($97 a month) and Agency ($497 a month). On Free, reveal the addresses one by one as in Step 4. The body takes the same filters as search, a limit and a column preset. The walkthrough uses 25 rows.
curl -s https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "ca-vp-sales",
"filters": {
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"employeeRange": ["51-200", "201-500"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
},
"limit": 25,
"preset": "full"
}' | jq '.data | {id, status, requested, pollAfter}'Poll GET /v1/exports/{id} until status is done, then fetch GET /v1/exports/{id}/download. Rows are deduplicated by person_id, and every row costs 1 record whatever the columns.
An export reserves up to limit records while it runs and bills each row it writes. One export takes up to 50,000 rows, and an account takes 500,000 a day. On a paid plan you can run the same export from the Exports page in the app. It shows the record price before you start.
Step 6: Clean the CSV before it reaches your sequencer
Drop what should not be mailed, then load the file. Three checks are enough. The first keeps only the mailable statuses, using the column names from the export header. The second drops anyone already in your CRM or on your suppression list.
mlr --icsv --ocsv filter '
$email_1_status == "verified" ||
$email_1_status == "catch_all_valid" ||
$email_1_status == "catch_all"' ca-vp-sales.csv > mailable.csv
mlr --icsv --ocsv join --np --ul -j email_1_address -f suppress.csv mailable.csv > send.csvThird, mail in small batches and watch the first bounces before the rest go out. Each record also carries its own fetched_at date, and the dataset refreshes monthly. A list you exported six months ago is older than the data behind it. Re-export before a new campaign instead of re-mailing the old file. The cost of decay is covered in email list churn.
What you get
Step 2 is free. We ran it on 2026-10-21 with leadocean_count_leads for VPs in Sales & Business Development in Canada, changing one filter at a time. Mailable rows use the tool's default (verified, catch_all_valid or catch_all) with no email filter passed.
| Filter set | People | Exact |
|---|---|---|
Every person, any email status (totalAll) | 18,800 | Yes |
| Mailable (default) | 5,574 | Yes |
emailStatus verified only | 1,854 | Yes |
| Mailable (default), company size 51-200 or 201-500 | 1,300 | Yes |
Each row is a count of people, not a promise of replies. The mailable count is about 30% of everyone matching the role and country (5,574 of 18,800). The rest carry a weaker status or no email. Adding the size filter shrinks the list to 1,300 people you can message in one sentence.
A revealed person comes back in the shape below. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "...": "name, title, company" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{ "email": "jane.doe@acme.com", "type": "work", "status": "verified" }
]
}
}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | The x-api-key header is missing, or the key is wrong or revoked. | Copy a fresh key from the app and reload $LEADOCEAN_API_KEY. |
| 402 | You have fewer records left than the call needs. On Free the 1,000 are a one-off allowance. | Check GET /v1/account, shrink limit, or move to Solo ($97) or Agency ($497). |
| 429 | Above your plan's rate limit: 5 a second on Free, 50 on Solo, 200 on Agency. | Wait the Retry-After seconds and retry. |
| Empty results | A filter value is not an exact enum string, such as Vice President for VP. | Call leadocean_list_enum_values for job_level and pass the exact string. |
| 404 on enrich | We hold nothing for that person. | Nothing was charged. Try another person_id. |
| Export refused | The export is over 50,000 rows, or the account is over 500,000 rows for the day. | Split the job across requests and days, or narrow the filters. |
FAQ
How big should a B2B email list be?
As small as the offer allows. Size with count=true and add filters until the audience is one you can describe in a sentence. A tight list of a few hundred beats a broad list of 50,000.
Should I mail catch_all addresses?
Yes, at low volume. catch_all means the domain accepts any mail, so the mailbox is unproven. catch_all_valid is better and verified is best. Treat a catch-all batch as a test, not a send-to-all.
How many people can I build on the free plan?
You have 1,000 records, one-off. A search row costs 1 record. Export is not on Free. A reveal on top of a search costs 1 more, so a searched-then-revealed contact costs 2. Counts are free.
Can I do this from Claude instead of curl?
Yes. The hosted MCP server at https://api.leadocean.io/mcp has 13 tools, and leadocean_count_leads is free. Sign in with OAuth, no key to paste.
Can I buy a ready-made list instead?
You can, and the list providers compared show the trade. A bought file is fixed on the day it ships. A query is current each time you run it. If you already have a list, check it with the email hygiene tools for B2B first.
Size your first list free, then export only the people you will mail
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →