Query a people database instead of scraping pages. A scraper breaks when the page layout changes. A query returns the same fields every time.
This guide uses the LeadOcean API. It covers 693M people, and you never touch a browser.
TL;DR
- Time: about 20 minutes from API key to a CSV on disk.
- Records spent: about 27 on the example run. Counting is free, so size the list before you pull anything.
- Result: filtered profile rows, one fully enriched profile with career history, and a CSV export for the rest.
Prerequisites
- A LeadOcean account on the Free plan: 1,000 records for life, no card, 5 requests a second.
- An API key stored as
$LEADOCEAN_API_KEY. curland Python 3.9 or later (only for Step 4).- A target in mind: a job level, a function and a country is enough.
Every request goes to https://api.leadocean.io with the header x-api-key.
Step 1: Size the list for free
Count before you pull. A count costs nothing and tells you whether the filter is too wide or too narrow.
Send count=true as a query parameter and limit 1 in the body. The JSON body rejects unknown fields, so the count flag must not go there.
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 '{
"jobLevel": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"limit": 1
}'data comes back empty and meta.countOnly is true. meta.total is capped at 100,000, so a bigger audience shows as a floor. Filter values are exact strings, so check them with GET /v1/enums/job_level first.
Step 2: Pull one page of profiles
Drop count and set a page size. limit runs from 1 to 100, and the default is 25. Each person returned costs 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 '{
"jobLevel": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"profileKeywords": ["outbound"],
"emailStatus": ["verified"],
"limit": 25
}'profileKeywords matches the person's own profile text. That is the filter a scraper user usually wants: "people whose profile mentions X". Keep the first run at 25 rows.
Step 3: Enrich one profile for career history
Search rows are light. To get the full profile, enrich one person. Send exactly one of linkedin_url, email, phone or person_id. Search rows carry meta.source_ids.person_id, so you can enrich anyone you found without carrying a URL.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"linkedin_url": "https://www.linkedin.com/in/jane-doe-example"}'Enrich returns resume_data since October 2026: past roles, education, certifications and awards, each when we hold them. Past employers carry no company_id yet. A person we hold nothing for is a 404 and costs nothing. Pass "reveal_email": true to get addresses and numbers at no extra record.
Step 4: Page through the rest by cursor
Pass back meta.nextCursor exactly as returned. Cursors are signed, so an edited one is refused with 400.
import os, requests
URL = "https://api.leadocean.io/v1/people/search"
HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
body = {
"jobLevel": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"profileKeywords": ["outbound"],
}
rows, cursor = [], None
while len(rows) < 200:
page_body = dict(body, limit=100)
if cursor:
page_body["cursor"] = cursor
r = requests.post(URL, headers=HEADERS, json=page_body, timeout=30)
r.raise_for_status()
page = r.json()
rows += page["data"]
cursor = page["meta"].get("nextCursor")
if not cursor:
break
print(len(rows), "profiles")One search pages to 10,000 rows. Past that, meta.depthCapped is true and no cursor comes back. Narrow the filters and run several smaller searches instead of paging harder.
Step 5: Export to CSV when you need thousands
A loop is fine for hundreds of rows. For more, use POST /v1/exports. One export takes up to 50,000 rows, and the account ceiling is 500,000 a day. Bulk CSV export is not on Free.
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-directors",
"filters": {
"jobLevel": ["Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"]
},
"limit": 5000,
"preset": "search"
}'The call returns 202. Poll GET /v1/exports/{id}, then fetch GET /v1/exports/{id}/download. The search preset has 27 columns and no contact points. The default full preset has 45 columns and needs the enrich scope. Every row costs one record either way.
What you get
Step 1 ran on 2026-10-21 through the LeadOcean MCP tool leadocean_count_leads, with the same filter (Director, Sales & Business Development, US).
{
"total": 322167,
"totalIsExact": true,
"totalAll": 825690,
"distinctCompanies": 99327,
"credits": 0
}That is 322,167 people with a verified, catch-all-valid or catch-all address. Without an email filter the same filters match 825,690. The count spent no records.
A search row follows this shape (response shape from the API reference, placeholder values).
{
"success": true,
"data": [
{
"profile_data": { "...": "name, headline, location" },
"contact_data": {
"has_email": true,
"has_phone": false,
"email_status": "verified",
"email_type": "work",
"contact_current_experiences": [
{ "...": "current title and employer (Acme)" }
]
}
}
],
"meta": { "count": 25, "nextCursor": "<opaque cursor>" }
}Treat the inner field names as indicative and read the real keys from GET /v1/people/search in the API reference.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported in this shell |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After, or sleep between pages |
| 402 | Records spent (Free is 1,000 for life) | Count first, narrow the filter, or move to Solo at $97 a month |
Empty data | An enum value that is not an exact string, such as director for jobLevel | List values with GET /v1/enums/job_level and copy them |
400 export_limit_rows | More than 50,000 rows in one export | Split the filter and run several exports |
| 400 "Invalid cursor" | The cursor was edited, or came from a different search | Pass meta.nextCursor back unchanged on the same filters |
FAQ
Is scraping professional profiles legal?
That depends on the site's terms, your jurisdiction and what you do with the data, and this guide is not legal advice. Read the site's terms and its robots.txt (RFC 9309, checked September 2026). A database query avoids the question of crawling a site yourself.
Why not use a no-code scraper?
Tools such as Clay describe a browser extension for list-style pages and AI columns that read a public profile page (Clay guide, October 2026). They fit one-off pulls. For a repeatable list, a filtered query is shorter and the fields stay the same.
When should I still use a scraper?
Use a scraper only for a page no database holds, such as one company's team page or a conference speaker list. Pull names and domains with a script, then resolve each person through the API. The guide on scraping a directory shows that pattern.
| Scraper | Database query | |
|---|---|---|
| Breaks when the layout changes | Yes | No |
| Needs proxies and a browser | Usually | No |
| Sizing before you pull | No | Free count |
| Good for | A page no database holds | Lists of people by filter |
Define who you want first. A written ICP turns "everyone with a profile" into a filter you can count. For the concept, see lead scrape. More guides are on the blog.
How many profiles can I get on the Free plan?
1,000 records, for life, with every feature and the whole database. Counts, filter lookups and enrichments that find nothing cost nothing. Paid plans and rate limits are on the pricing page.
How fresh is the data?
The dataset is refreshed monthly. Each record also carries its own fetched_at date.
Pull your first 25 profiles without writing a scraper
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →