An ICP only matters once it is a query you can run. This guide turns a written profile into LeadOcean filters, sizes the market for free, and tests the fit on 25 records.
Most ICP guides stop at a worksheet. This one ends with a count and a sample you can inspect.
Here is the logic. A written ICP is a hypothesis about who buys. A filter set is a test of that hypothesis against a real database. The count tells you if the market is big enough, and the sample tells you if the filters describe the people you meant.
TL;DR
- Write the ICP as two filter sets, one for companies and one for the people inside them. Size both with free count calls.
- Total time is about 45 minutes. Total spend is about 30 records out of the 1,000 on the free plan.
- Test the filters on 25 rows before you export anything. If fewer than 20 look right, tighten the filters, not the sample.
Prerequisites
- A LeadOcean account on the free plan. No card needed.
- An API key exported as
$LEADOCEAN_API_KEY. curlandjq, or any HTTP client.- A one-paragraph ICP. Our example: "US software companies with 51 to 200 employees, selling to VPs and C-level leaders in sales.".
If you have no written profile yet, start with What Is an ICP? or the ICP prompt.
Step 1: Translate the ICP into filters
Split the ICP into firmographics (the company) and persona (the buyer). Firmographics become company filters. Persona becomes people filters.
Every filter is an array of strings, and enum values are exact. Check them before you guess.
curl -s "https://api.leadocean.io/v1/enums/job_level" \
-H "x-api-key: $LEADOCEAN_API_KEY"GET /v1/enums lists every enum. GET /v1/enums/{name} returns one, such as industry, job_function or employee_range, and takes q to search it.
The same lookup exists as the free MCP tool leadocean_list_enum_values. Here is the ICP as a filter object:
{
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"hqCountry": ["US"],
"jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"]
}jobLevel takes C-Team, VP, Director, Manager, Staff or Other. jobFunction takes names like Sales & Business Development. Copy values exactly as the enum returns them.
Step 2: Size the companies (free)
Count the companies first. If the market is 40 companies, you have an account list problem, not a data problem.
curl -s -G "https://api.leadocean.io/v1/companies/count" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
--data-urlencode "industry=Software Development" \
--data-urlencode "employeeRange=51-200" \
--data-urlencode "hqCountry=US" | jq '.data.total'Array filters on this GET take comma-separated values, such as employeeRange=51-200,201-500. The call is free and returns an exact total plus breakdowns by industry, headcount band, HQ country, HQ state and NAICS. Inside Claude or ChatGPT, the same call is the leadocean_count_companies MCP tool.
Read the state breakdown. If one state holds a quarter of the market, that is a territory decision, not a filter tweak.
Also read the NAICS breakdown. If one code dominates, your industry label is doing the work. If the codes are scattered, the label is loose and the sample step will show it.
Step 3: Size the people (free)
Now count the buyers at those companies. Send count=true as a query parameter with limit 1. The JSON body stays flat.
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 '{
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"hqCountry": ["US"],
"jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"],
"limit": 1
}'The response has meta.total (capped at 100,000) and meta.countOnly: true, with an empty data array. It spends no records.
Counts through the MCP tool leadocean_count_leads also return distinctCompanies, which shows how many employers the people span. That ratio tells you if you have one buyer per account or several.
Step 4: Decide what "reachable" means
A count of everyone is not a count of people you can write to. leadocean_count_leads counts verified, catch_all_valid and catch_all emails by default. Say which filter you used when you quote a number.
Add hasPhone or emailStatus only if your motion needs them. Each one narrows the list and changes what the total means.
{
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
}Keep the mailable count as the number you plan around. The unfiltered number is a ceiling.
A gap between the two is normal. In our example the mailable count is 2,806 and the unfiltered count is 7,412. Plan outreach on the smaller number and treat the rest as upside you can verify later.
Step 5: Pull a 25-row sample and grade it
Counts tell you size. A sample tells you fit. Pull 25 people and read them.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"hqCountry": ["US"],
"jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"],
"limit": 25
}' | jq '.data | length'This spends 25 records. Grade each row on three questions: is the company really a fit, is the title really the buyer, is the email status usable. Write the result as pass or fail per row.
If 20 or more pass, the filters are good. If fewer, find the pattern in the failures. Usually it is an industry label that is too broad, which you fix with companyKeywords or excludeIndustry.
Step 6: Check the buyer's history (optional)
Enrich a handful of passing rows by person_id to see career history. Since October 2026 person enrich returns past roles, education and certifications. Past employers have no company_id yet.
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_STEP_5"}'Enrich takes exactly one of linkedin_url, email, phone or person_id. An enrichment that finds nothing is a 404 and costs nothing. Five calls cost at most five records.
Use the history to refine the persona. If most passing buyers came from one prior industry, add that to your next filter set.
Step 7: Save the ICP as a filter file
Keep the ICP in version control, not in a slide. One JSON file per segment.
{
"name": "us-software-51-200-sales-leaders",
"companies": {
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"hqCountry": ["US"]
},
"people": {
"jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"]
},
"checked": "2026-10-21"
}Rerun the two counts monthly. The dataset refreshes monthly, so the totals move. When you are ready to act on a segment, export it with POST /v1/exports, or use the Exports page in the app.
For scoring accounts against the profile, see ICP scoring tools or run the ICP qualifier skill in Claude.
What you get
These are free counts for the example ICP. I ran them on 2026-10-21 with leadocean_count_companies and leadocean_count_leads.
Companies: industry Software Development, employeeRange 51-200, hqCountry US
total: 9,261 (exact)
top HQ states: California 2,398, New York 842, Texas 776, Florida 471, Massachusetts 409
People: same company filters, jobLevel VP or C-Team, jobFunction Sales & Business Development
mailable (verified, catch_all_valid, catch_all): 2,806 (exact)
all matching people, no email condition: 7,412
distinct companies spanned: 1,546Read it this way: 9,261 companies fit the firmographics, but only 1,546 of them hold a mailable sales leader in the data. The 2,806 mailable people is your realistic first list.
A person record from the sample step has the shape below. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_current_experiences": [
{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }
]
},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
],
"meta": { "total": 2806, "nextCursor": "..." }
}The person_id for Step 6 sits at meta.source_ids.person_id on each row.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Check $LEADOCEAN_API_KEY is exported and sent as x-api-key. |
| 429 | Above the plan's rate limit (5 requests a second on Free, 200 on Agency) | Honor the Retry-After header and slow the loop. |
| 402 | The 1,000 free records are spent | Counts stay free. Move to a paid plan on pricing to keep searching. |
| Empty results | A wrong enum value, such as VP Sales for jobLevel | Look up exact values with /v1/enums/job_level or leadocean_list_enum_values. |
meta.depthCapped is true | One search pages to 10,000 rows | Narrow the filters instead of paging deeper. |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the segment into smaller filter sets. |
FAQ
How many filters should an ICP have?
Start with four: industry, headcount, country and buyer level. Add more only when the 25-row sample fails on a pattern a new filter would fix.
Does sizing the ICP cost records?
No. Company counts, people counts and enum lookups are free. Records are spent when a search returns rows or an enrich finds a person.
Can I do this without code?
Yes. Connect the hosted MCP server to Claude and ask for the counts in plain English. The tools leadocean_count_companies and leadocean_count_leads run the same free sizing calls.
What if my ICP is based on funding or revenue?
Those filters apply to company search, but they do not work in free people counts. Size the companies first, then count people on the filters that do work.
Size your ICP before you buy a list
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →