B2B data sales means picking an audience, checking it is big enough, and pulling only the rows you will contact. This guide does that with four calls against the LeadOcean API.
TL;DR
- Size the list first with a free count, then spend records only on the page you need.
- Takes about 30 minutes and spends about 30 records, well inside the Free plan's 1,000 records for life.
- Sizing, filter lookups and a 404 enrich cost nothing. A search page of 25 costs 25 records.
Prerequisites
- A LeadOcean API key from app.leadocean.io. The Free plan needs no card.
- The key exported as an environment variable:
export LEADOCEAN_API_KEY=... curlandjq.- Free rate limit: 5 requests a second. Agency allows 200. Check pricing for your plan.
Step 1: Define the profile with exact filter values
Write the profile as filters before you touch the API. Enum filters take exact strings, so a typo returns nothing.
curl -s "https://api.leadocean.io/v1/enums" \
-H "x-api-key: $LEADOCEAN_API_KEY" | jq 'keys'Our profile: VPs of Sales & Business Development, in the US, at companies with 51-200 employees. In filter terms that is jobLevel: ["VP"], jobFunction: ["Sales & Business Development"], country: ["US"], employeeRange: ["51-200"]. In an MCP client you can ask leadocean_list_enum_values for the same strings.
Step 2: Size the list for free
Add count=true and limit=1 as query parameters. The body stays flat JSON with array filters.
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["51-200"]
}' | jq '.meta'The response has data empty and meta.countOnly true. meta.total is capped at 100,000. If your total is bigger, narrow the filters before you pull.
Step 3: Pull one page
Now spend records. Ask for 25 rows and keep the cursor for the next page.
curl -s -X POST "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": ["US"],
"employeeRange": ["51-200"],
"limit": 25
}' > page1.json
jq '.meta' page1.jsonlimit runs 1 to 100. Pass meta.nextCursor back as cursor in the next body to get page two. One search pages to 10,000 rows. If meta.depthCapped is true, narrow the filters instead of walking further.
Step 4: Enrich the people you will contact
Search returns the profile, and each row carries its id at meta.source_ids.person_id. Enrich takes exactly one of linkedin_url, email, phone or person_id. Set reveal_email to get the address.
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_SEARCH", "reveal_email": true}'A person LeadOcean holds nothing for returns 404 and costs nothing. Read email_status before you send, because bounced emails are not refunded. Since October 2026 enrich also returns resume_data (past roles, education, certifications) when held.
Step 5: Export when the list is final
For lists over a few hundred rows, use POST /v1/exports. One request takes up to 50,000 rows and the daily cap is 500,000 rows. Bulk CSV export is not on Free, so this step needs Solo ($97 a month) or higher. The app has the same thing with a filter builder, column picker and price preview on its Exports page.
curl -s -X POST "https://api.leadocean.io/v1/exports" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "US sales VPs, 51-200",
"filters": {
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["51-200"]
},
"limit": 5000,
"preset": "full"
}'Poll GET /v1/exports/{id} until status is done, then fetch GET /v1/exports/{id}/download. Every row costs one record.
What you get
This is a real count from the leadocean_count_leads MCP tool on 2026-10-21, with the filters from Step 1.
{
"total": 18567,
"totalIsExact": true,
"totalAll": 60456,
"distinctCompanies": 12192,
"credits": 0
}total counts mailable addresses only (verified, catch_all_valid or catch_all). totalAll is the same filters with no email condition. credits: 0 confirms the count was free.
A search row looks like this. This is the 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",
"email_type": "work",
"contact_current_experiences": [
{ "company_name": "Acme", "company_domain": "acme.com", "job_title": "VP Sales" }
]
},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
],
"meta": { "count": 25, "credits": 25, "nextCursor": "eyJ..." }
}Search rows carry flags, not addresses. Enrich the row by meta.source_ids.person_id to get the email.
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 your plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After, or move to a plan with a higher limit |
| 402 | Records spent (Free is 1,000 for life) | Upgrade at pricing. Paid plans slow at the fair-use pace instead of stopping |
Empty data | Enum value not an exact match, such as Vp or United States | Look up values with /v1/enums or leadocean_list_enum_values |
meta.depthCapped is true | One search stops at 10,000 rows | Add filters and run several narrower searches |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the export by country or headcount band |
FAQ
How many records does this recipe spend?
About 30: 25 for one search page plus up to 5 enrichments. The count, the enum lookup and any 404 enrich are free.
Can I do this without writing code?
Yes. The app at app.leadocean.io has an Exports page with a filter builder and a price preview before you download. It has no sequencer, dialer or CRM.
Is the count a list of emails I can send to?
The default count is mailable: verified, catch_all_valid and catch_all. Pass emailStatus to change it, and label the result by what you filtered.
How fresh is the data?
The dataset is refreshed monthly. Each record also carries its own fetched_at date.
Where does this fit with firmographic and CRM data?
Read our guides on firmographic data providers and what CRM enrichment adds. For agent-driven workflows see B2B sales agents.
Size your first B2B list for free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →