TL;DR
- A targeted list is your ICP written as filters, then tightened one filter at a time while you watch the count fall.
- It takes about 25 minutes. Sizing is free, and the example list spends about 510 of your 1,000 free LeadOcean records.
- Count first, pull ten rows second, pull the full list last. You never pay for a list you have not sized.
Prerequisites
- A LeadOcean API key from app.leadocean.io/register. The free plan has 1,000 records, one-off, no card.
- The key in your shell:
export LEADOCEAN_API_KEY=your_key_here. curlandjq. Any HTTP client works the same way.- Your last ten closed-won accounts, open in another tab. They are the test for every filter you add.
Step 1: Write the ICP as a filter file
Write the ICP as one JSON object before you run anything. If a criterion cannot be written as a filter, it is not ready to source against.
Split it into three groups: who the company is, who the person is, and what you exclude. The values below are an example for a seller of sales tooling.
{
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"jobFunction": ["Sales & Business Development"],
"jobLevel": ["VP", "Director"],
"excludeTitle": ["assistant", "intern", "associate"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
}Every filter is an array. Company filters (industry, employeeRange) and person filters (jobFunction, jobLevel) live in the same flat body. country is where the person sits. Use hqCountry when you mean the company's headquarters.
Clay's guide covers the same ground in six steps, from ICP through sourcing, enrichment and scoring (Clay, September 2026). It is good on why a list fails at definition. Here you get the queries. Scoring is outside this recipe, so do it in your own sheet or CRM.
Step 2: Look up the exact enum values
Enum values are exact strings, so look them up before you type them. A wrong value gives you an error or an empty list. The lookups are free.
curl -s "https://api.leadocean.io/v1/enums/industry?q=software" \
-H "x-api-key: $LEADOCEAN_API_KEY"
curl -s "https://api.leadocean.io/v1/enums/job_function" \
-H "x-api-key: $LEADOCEAN_API_KEY"industry has 534 values and many are near-duplicates. "Software Development" and "Computer Software" are two separate values. Pass both if your closed-won accounts could sit under either, then compare the counts.
Step 3: Shrink the list one filter at a time
Add count=true as a query parameter and send limit 1 in the body. You get meta.total and an empty data array, and no records are spent. This loop adds one criterion per line, so you see what each one removes.
BASE='{"limit":1,"country":["US"],"jobFunction":["Sales & Business Development"],"jobLevel":["VP","Director"],"emailStatus":["verified","catch_all_valid","catch_all"]}'
count() { # $1 = extra filters as JSON
jq -n --argjson b "$BASE" --argjson x "$1" '$b + $x' |
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 @- |
jq -c '.meta | {total, totalIsExact}'
}
count '{}'
count '{"industry":["Software Development"]}'
count '{"industry":["Software Development"],"employeeRange":["51-200","201-500"]}'
count '{"industry":["Software Development"],"employeeRange":["51-200"]}'
count '{"industry":["Software Development"],"employeeRange":["51-200"],"technologies":["HubSpot"]}'count=true goes in the query string, because the JSON body rejects unknown fields. The total is capped at 100,000, and totalIsExact tells you when it is a floor.
Stop when the list is small enough for your team to work and each cut still matches your closed-won accounts. A list of 3,000 people that fits is worth more than 450,000 that mostly does not. If a filter drops a closed-won account you know is a fit, loosen it.
Step 4: Cut the noise
Exclusions are cheaper than cleanup. Add excludeTitle and excludeCompanyName to remove roles and companies you will never sell to, such as agencies, your own customers or competitors.
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 '{
"limit": 1,
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"jobFunction": ["Sales & Business Development"],
"jobLevel": ["VP", "Director"],
"excludeTitle": ["assistant", "intern", "associate"],
"excludeCompanyName": ["Acme"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
}' | jq '.meta | {total, totalIsExact}'The emailStatus line keeps addresses worth sending to and drops the rest. LeadOcean does not refund bounces, so read email_status before you send.
Step 5: Spot-check ten rows
Pull ten rows before you export hundreds. Remove count, set limit to 10, and read what comes back.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"limit": 10,
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"jobFunction": ["Sales & Business Development"],
"jobLevel": ["VP", "Director"],
"excludeTitle": ["assistant", "intern", "associate"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
}' | jq '.data[] | {has_email: .contact_data.has_email, email_status: .contact_data.email_status}'This costs 10 records. Search rows carry flags, not addresses, so you see that a deliverable email exists before you buy it. Open the profile and company site for each row and ask one question: would we have sold to this account? Two misses out of ten means a filter is too loose. Go back to Step 3.
Step 6: Pull the list, or export it as CSV
On the free plan, page the Step 5 search. Set limit to 100 and pass meta.nextCursor back as cursor until you have 500 rows: five pages, 500 records.
Bulk CSV export is not on Free. It is included on Solo ($97 a month) and Agency ($497 a month, 50k rows a file). Export with POST /v1/exports. It takes the same filters, minus limit, count, cursor and facets, plus a row limit. One export runs up to 50,000 rows, and the daily ceiling is 500,000 rows.
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 and Director Sales, US software, 51-200",
"filters": {
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"jobFunction": ["Sales & Business Development"],
"jobLevel": ["VP", "Director"],
"excludeTitle": ["assistant", "intern", "associate"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
},
"limit": 500,
"preset": "full"
}'The answer is HTTP 202 with an export id and status: queued. Poll GET /v1/exports/{id} until status is done, then read the signed downloadUrl. Each row costs one record, so this export spends at most 500.
Prefer no code? On a paid plan, the Exports page in the LeadOcean app has a filter builder, a column picker, the record price before you start and a Download button.
What you get
Counts below come from the free leadocean_count_leads MCP tool on 2026-10-21, with no email filter passed. The tool's default counts mailable people (best email verified, catch_all_valid or catch_all). Filters: people in the US, job function Sales & Business Development, job level VP or Director. Each call returned totalIsExact: true, credits: 0 and an empty unsupportedFilters list.
| Filters added | Mailable people |
|---|---|
| ICP so far (person filters only) | 451,661 |
| + industry Software Development | 36,632 |
| + employeeRange 51-200 and 201-500 | 8,986 |
| + employeeRange 51-200 only | 4,926 |
+ excludeTitle assistant, intern, associate | 4,907 |
+ technologies HubSpot (on the 51-200 list, before excludeTitle) | 2,981 |
One filter took the list from 451,661 to 36,632. That is the pattern to expect: industry and size do most of the cutting, and exclusions trim the edge. These are counts of people, not companies. The 4,926 people on the 51-200 list span 2,348 employers (distinctCompanies).
A search row looks like this. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": [
{
"profile_data": { "...": "identity and headline fields" },
"contact_data": {
"has_email": true,
"has_phone": false,
"email_status": "verified",
"email_type": "work"
},
"meta": { "...": "person_id and source fields" }
}
],
"meta": { "count": 1, "limit": 10, "credits": 1, "nextCursor": "<opaque string>" }
}Paginate by passing meta.nextCursor back unchanged. Use exports for anything larger than a few thousand rows.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Check x-api-key and that $LEADOCEAN_API_KEY is set in this shell. |
| 429 | Above your plan's rate limit: 5 a second on Free, 50 on Solo, 200 on Agency | Wait for Retry-After and retry. Add a short sleep to loops. |
| 402 | The free 1,000 records are spent | They are a one-off and do not reset. Move to Solo ($97) or Agency ($497) to keep going. |
| Empty or tiny results | A misspelled enum value, or jobLevel written as vp | Look up values with GET /v1/enums/{name}. jobLevel is VP, seniority is lowercase vp. |
400 on count=true | count sent in the JSON body | Move it to the query string. The body rejects unknown fields. |
| Export refused | More than 50,000 rows in one request, or past the daily ceiling of 500,000 | Split the export into smaller runs, or wait for the daily reset. |
FAQ
How big should a targeted prospect list be?
Size it to what your team can work in a quarter, not to what the database holds. For one rep running sequences, a few hundred to a few thousand people is workable. If the REST count stops at 100,000 with totalIsExact false, the filters are still too wide.
Should I start from companies or from people?
Start from people if you know the buyer's title, as in this recipe. Start from companies with POST /v1/companies/search if the account is the unit you rank. Then call GET /v1/companies/{domain}/people for the buyers at each account.
Can I add buying signals such as funding or hiring?
Funding filters exist in the search API, and so does a technology filter, as in the tech-stack list recipe. Hiring signals are not a filter. Count funding filters at search time, not through the MCP count tool, which does not support all of them.
How do I get phone numbers for the list?
Search rows say whether a number exists with has_phone. For numbers, call GET /v2/people/phone, which costs one record per call, including a call that finds nothing. The cold-calling list recipe covers that flow.
What does this cost on the free plan?
Sizing costs nothing. The ten-row spot-check costs 10 records and a 500-row pull costs 500. That leaves about 490 of your 1,000 free records. CSV export needs Solo ($97 a month) or Agency ($497); see pricing.
If you are comparing tools for the enrichment step, these eight enrichment tools are worth a look.
Build your first targeted prospect list free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →