Send the domains of your target accounts and the titles you want. LeadOcean returns the people who hold those titles at those companies. You can size the list for free before you spend a record.
TL;DR
- One search request takes
domainandtitleas arrays and returns matching people, up to 100 a page. - It takes about 15 minutes. A first run spends about 26 records: 25 for one search page, 1 for one enrichment. Counts cost nothing.
- Title matching is keyword matching, so pair
titlewithjobLevelorseniorityto keep the list clean.
Prerequisites
- A LeadOcean account. The Free plan is enough: 1,000 records for life, no card, 5 requests a second.
- An API key from app.leadocean.io, exported as
$LEADOCEAN_API_KEY. curlandjq. Any HTTP client works.- A list of target account domains, such as
acme.com. This page uses placeholders.
Step 1: Pick titles and a level, not only keywords
Titles are messy. "VP Sales", "Vice President, Sales" and "SVP of Revenue" are the same buyer. A bare keyword filter will miss some and catch noise.
Use three filters together:
title: an array of keywords. Each value is a phrase. Wrap a value in brackets for an exact match.jobLevel:C-Team,VP,Director,Manager,StafforOther. Exact strings, case matters.jobFunction: the department, for exampleSales & Business Development.
{
"title": ["VP Sales", "Head of Sales", "Chief Revenue Officer"],
"jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"]
}The level and function filters do the heavy lifting. The title keywords then narrow within them. Use excludeTitle to drop noise such as ["assistant", "intern"].
Two patterns work well. For a broad net, use jobLevel and jobFunction with no title. For one exact role, use a bracketed title such as ["[Chief Revenue Officer]"]. Run both as counts and compare the totals. The gap tells you how many people hold the role under a different name.
Step 2: Resolve enum values before you send them
Enum values are exact strings. A wrong one returns an empty result, not an error. Look them up first. It is free.
curl -s "https://api.leadocean.io/v1/enums/job_level" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jqGET /v1/enums lists every enum name, and GET /v1/enums/{name} returns its values. In an MCP client, call leadocean_list_enum_values with name: "job_level" or name: "job_function" instead. Both calls spend no records.
Step 3: Add your target accounts as domains
Domains are the cleanest way to say "inside these accounts". Names collide. Domains do not.
{
"domain": ["acme.com", "globex.example", "initech.example"],
"title": ["VP Sales", "Head of Sales", "Chief Revenue Officer"],
"jobLevel": ["VP", "C-Team"]
}For a single account, there is a shortcut: GET /v1/companies/{domain}/people lists the people at one company. It stops at 10,000 rows.
Have names but no domains? Resolve the companies first with POST /v1/companies/search, then pass the domains it returns. Check the list size with GET /v1/companies/count (free) before you build on it.
Step 4: Size the result for free
Never page blind. Send the same body with count=true as a query parameter and limit set to 1. The data comes back empty, meta.total holds the count, and nothing is spent.
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 '{
"domain": ["acme.com", "globex.example", "initech.example"],
"title": ["VP Sales", "Head of Sales", "Chief Revenue Officer"],
"jobLevel": ["VP", "C-Team"],
"limit": 1
}' | jq '.meta'Put count=true in the URL. The JSON body rejects unknown fields. meta.total caps at 100,000.
If the total is zero, loosen one filter at a time. Drop jobLevel first, then widen title.
Step 5: Run the search and page through it
Now spend records. Set limit between 1 and 100 (default 25). 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 '{
"domain": ["acme.com", "globex.example", "initech.example"],
"title": ["VP Sales", "Head of Sales", "Chief Revenue Officer"],
"jobLevel": ["VP", "C-Team"],
"limit": 25
}' > page1.json
jq '.meta' page1.jsonFor the next page, send the cursor value from meta.nextCursor in the body. One search pages to 10,000 rows. If meta.depthCapped is true, there is no nextCursor: narrow the filters instead of walking the index.
Step 6: Enrich only the people you keep
Search gives you the person. Enrichment gives you the contact data. Pass 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_STEP_5", "reveal_email": true}'There is no name plus domain lookup. Search by domain first, then enrich by person_id. An enrichment that finds nothing returns 404 and costs nothing. The current role is in contact_data.contact_current_experiences[]. Past roles are in resume_data.experiences[]. Each search row carries its id at meta.source_ids.person_id.
Step 7: Export a large list
Past a few hundred rows, export instead of paging. POST /v1/exports takes the same filters, up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free. It is included on Solo ($97 a month). Agency ($497 a month) gets 50,000 rows a file.
You can also build the same export in the app at app.leadocean.io: filter builder, column picker, a price preview, then CSV download.
Step 8: Check coverage per account
A search returns people, but you often want to know which target accounts came back empty. Group the first page by company and diff it against your list.
jq -r '.data[] | .contact_data.contact_current_experiences[0].company_domain' page1.json \
| sort -u > found.txt
sort -u targets.txt | comm -23 - found.txt > missing.txtThe current employer's domain sits on the first entry of contact_current_experiences. Run the check on a full export, not one page, or every account past page one looks missing.
For the missing accounts, loosen the filters on that account alone. Drop jobLevel, widen title to the department head, or switch to GET /v1/companies/{domain}/people and read the titles yourself. Accounts with no match at all are worth knowing about: they are the ones where the buyer is not in the data, and a different channel is needed.
Do it in an AI client instead
The hosted MCP server at https://api.leadocean.io/mcp signs in with OAuth, no key pasted. Ask for what you want:
Count VPs and C-level people in Sales & Business Development at
acme.com, globex.example and initech.example. Use leadocean_count_leads
first. Do not search until I confirm.The client calls leadocean_count_leads, which is free, and reports the total before anything is spent.
What you get
A count run on 2026-10-21 with the MCP tool leadocean_count_leads, filters title: ["VP Sales", "Head of Sales"], country: ["US"], employeeRange: ["51-200"], default mailable email filter:
{
"total": 2639,
"totalIsExact": true,
"totalAll": 8926,
"distinctCompanies": 2253,
"credits": 0
}Read it like this: 2,639 mailable people (verified, catch-all valid or catch-all email) across 2,253 companies. Without the email condition, 8,926 people match. The count spent nothing.
Response shape from the API reference, with placeholder values:
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"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": "1234567" } }
}
],
"meta": { "count": 25, "limit": 25, "credits": 25, "nextCursor": "CURSOR_STRING" }
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-copy the key from app.leadocean.io and send it as x-api-key. |
| 429 | Requests above the plan's rate limit: 5 a second on Free, 200 on Agency | Wait for the Retry-After header, then retry. Batch domains into one request. |
| 402 | The 1,000 free records are spent | Upgrade to Solo ($97 a month) or Agency ($497 a month). Counts and enrichments that find nothing stay free. |
| Empty results | A wrong enum value, such as vp instead of VP for jobLevel | Check exact strings with /v1/enums or leadocean_list_enum_values. Loosen one filter at a time. |
| 404 on enrich | Nobody matched the input | Free. Try linkedin_url or email instead of person_id. |
| Export refused | Over 50,000 rows a request or 500,000 a day, or Free plan | Split the filters into slices. Bulk CSV export needs Solo or above. |
FAQ
Can I search titles inside a list of company names, not domains?
Yes, with companyName. Each value is a phrase, and brackets make it exact. Domains are safer, since names collide.
How do I avoid fake matches like "Assistant to the VP Sales"?
Use excludeTitle with ["assistant", "intern"] and set jobLevel. The level filter reads the seniority of the current role, not the words in the title.
How many records does a list of 500 accounts cost?
It depends on how many people match, and each person returned is one record. Count first. The count is free and shows meta.total and the number of distinct companies.
Does search return emails?
Search finds the people. Enrich with reveal_email to get the address, or export with email columns. Check contact_data.email_status before you send.
Can I search several buying roles in one call?
Not cleanly. Run one search per role, for example one for sales leaders and one for RevOps. Each body stays small, each count is free, and you can compare totals before you spend.
Related: target account list, find target accounts with a prompt, find people by job title and pricing.
Search any job title inside your target accounts, free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →