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

How-to guide

How to Build a Targeted Prospect List (With the LeadOcean API)

Write the ICP as filters, shrink the audience one criterion at a time with free counts, then pull the list or export it to CSV.

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

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.
  • curl and jq. 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.

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

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

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

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

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

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 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 addedMailable people
ICP so far (person filters only)451,661
+ industry Software Development36,632
+ employeeRange 51-200 and 201-5008,986
+ employeeRange 51-200 only4,926
+ excludeTitle assistant, intern, associate4,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.

json
{
  "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

ErrorCauseFix
401Missing, wrong or revoked keyCheck x-api-key and that $LEADOCEAN_API_KEY is set in this shell.
429Above your plan's rate limit: 5 a second on Free, 50 on Solo, 200 on AgencyWait for Retry-After and retry. Add a short sleep to loops.
402The free 1,000 records are spentThey are a one-off and do not reset. Move to Solo ($97) or Agency ($497) to keep going.
Empty or tiny resultsA misspelled enum value, or jobLevel written as vpLook up values with GET /v1/enums/{name}. jobLevel is VP, seniority is lowercase vp.
400 on count=truecount sent in the JSON bodyMove it to the query string. The body rejects unknown fields.
Export refusedMore than 50,000 rows in one request, or past the daily ceiling of 500,000Split 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 →