An internal recruiting tool is four API calls and a script: count the pool, search it, enrich the shortlist, export the rest. This guide builds that against LeadOcean's REST API with real queries. It takes about 45 minutes.
TL;DR
- Time: about 45 minutes with curl and a 30-line Python script.
- Cost: about 28 LeadOcean records on the example (25 search rows plus 3 enrichments). Counts are free.
- Result: a sourcing script that sizes a pool, lists candidates, pulls career history and writes a CSV your recruiters can open.
Prerequisites
- A LeadOcean account on the free plan: 1,000 records for life, no card, 5 requests a second. Start free.
- An API key from the app, exported as
$LEADOCEAN_API_KEY. - curl and Python 3 with
requestsinstalled. - A role you actually hire for. The example uses a Machine Learning Engineer in Austin.
Step 1: Size the talent pool for free
Count before you spend. Send count=true as a query parameter with limit 1. The call returns a total and no rows, so it spends nothing.
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 '{
"title": ["Machine Learning Engineer"],
"country": ["US"],
"city": ["Austin"],
"keywords": ["PyTorch"],
"limit": 1
}'Filters are arrays of strings. title matches job title keywords. keywords matches the person's own profile text: headline, summary, skills. meta.total is capped at 100,000. Enum filters such as jobFunction are exact strings, so check GET /v1/enums/job_function first.
The MCP tool leadocean_count_leads runs the same count from an AI agent, and leadocean_count_companies sizes a list of employers.
Step 2: Search and keep the person_id
Drop count=true and set a page size. 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 '{
"title": ["Machine Learning Engineer"],
"country": ["US"],
"city": ["Austin"],
"keywords": ["PyTorch"],
"excludeTitle": ["Intern"],
"limit": 25
}'Search rows are thin: no contact points. Each row carries meta.source_ids.person_id. Keep it. You enrich with that id in step 3.
Page with meta.nextCursor passed back as cursor. One search pages to 10,000 rows, then meta.depthCapped is true and no cursor comes back. Narrow the filters instead of walking the index.
Step 3: Enrich the shortlist with career history
Enrich takes exactly one of linkedin_url, email, phone or person_id. There is no name plus domain lookup, so search first, then enrich by id. Enrichment returns past roles, education, certifications and awards in resume_data, which is what a recruiter reads before outreach.
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": "1234567890", "reveal_email": true}'reveal_email defaults to false. Set it true to get the emails and phones we hold, at no extra record. An enrichment that finds nothing is a 404 and costs nothing. Past employers carry no company_id yet, so match former employers by name.
Step 4: Glue it into one script
This script searches, enriches the first three rows and writes a CSV. It is the whole tool in miniature.
import csv, os, requests
BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
body = {
"title": ["Machine Learning Engineer"],
"country": ["US"],
"city": ["Austin"],
"keywords": ["PyTorch"],
"limit": 25,
}
rows = requests.post(f"{BASE}/v1/people/search", headers=H, json=body).json()["data"]
out = []
for row in rows[:3]:
pid = row["meta"]["source_ids"]["person_id"]
r = requests.post(f"{BASE}/v1/people/enrich", headers=H,
json={"person_id": pid, "reveal_email": True})
if r.status_code == 404:
continue
person = r.json()["data"]
out.append({
"person_id": pid,
"email_status": person["contact_data"].get("email_status"),
"has_phone": person["contact_data"].get("has_phone"),
"past_roles": len(person.get("resume_data", {}).get("experiences", [])),
})
with open("shortlist.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["person_id", "email_status", "has_phone", "past_roles"])
w.writeheader()
w.writerows(out)Check email_status before anyone sends. There is no refund for bounced emails.
Design notes before you scale it
Three choices decide whether the tool stays cheap and useful.
- Count first, always. Put the free count behind a button in your UI. Recruiters see the pool size before any record is spent.
- Search wide, enrich narrow. Search rows carry no contact points. Enrich only people a recruiter has marked as a fit.
- Store the person_id. It lets you re-enrich the same person later without carrying a LinkedIn URL or email between calls.
Step 5: Export the full pool as a CSV
For hundreds of candidates, stop looping and export. One export takes up to 50,000 rows, and an account has 500,000 a day. Bulk CSV export is not on the free plan. It is included on Solo ($97 a month) and Agency ($497 a month, 50k rows a file).
curl -s -X POST "https://api.leadocean.io/v1/exports" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "ml-engineers-austin",
"filters": {
"title": ["Machine Learning Engineer"],
"country": ["US"],
"city": ["Austin"],
"keywords": ["PyTorch"]
},
"limit": 150,
"preset": "full"
}'The answer is a 202 with a job id. Poll GET /v1/exports/{id} until status is done, then GET /v1/exports/{id}/download. Every row costs one record. Prefer a UI? The Exports page in app.leadocean.io has the same filter builder, a column picker and a price preview.
What you get
Step 1 ran on 2026-10-21 through the leadocean_count_leads MCP tool, which is free. Query: title Machine Learning Engineer, country US, city Austin, keyword PyTorch.
{
"total": 74,
"totalIsExact": true,
"totalAll": 150,
"distinctCompanies": 58,
"credits": 0
}total counts people with a mailable email (verified, catch_all_valid or catch_all). The 74 work at 58 employers. totalAll is the same filters with no email condition: 150 people. Seniority in the mailable set: 71 entry, 1 senior. That is a small pool, so loosen city before you buy anything.
A wider query shows how the pool grows. Title Technical Recruiter, country US, counted 23,913 mailable people (74,953 with no email condition) on 2026-10-21. That is the list to size if you sell to recruiting teams.
An enrichment response (response shape from the API reference, placeholder values).
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"has_email": true,
"has_phone": false,
"email_status": "verified",
"contact_current_experiences": [ { "...": "current role" } ]
},
"resume_data": {
"experiences": [ { "...": "past role" } ],
"educations": [ { "...": "school" } ],
"certifications": [],
"awards": []
},
"meta": { "source_ids": { "person_id": "1234567890" } }
}
}Each past role in experiences[] carries a title, the company name, start and end dates and a job level. The API reference leaves the inner field names open, so read one live response before you map them. Each list appears only when we hold data for the person.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Re-copy the key from the app. Confirm $LEADOCEAN_API_KEY is set in that shell. |
| 429 | Above the plan's rate limit: 5 requests a second on Free, 200 on Agency | Honour Retry-After and sleep between calls. |
| 402 | Out of records | The free plan holds 1,000 records for life. Move to a paid plan, see pricing. |
| Empty results | A bad enum value, or filters too narrow | Fetch exact strings from GET /v1/enums/{name}. Loosen city, then count again. |
| 404 on enrich | Nobody matched that person_id | Skip the row. A 404 costs nothing. |
400 export_limit_rows | Asked for more than 50,000 rows in one export | Split the filters into several exports. |
FAQ
What data does a recruiting tool need?
Title, location, seniority, skills, past roles and a way to reach the person. Search gives the first four. Enrichment adds career history and contact points.
Can I search by skill?
Use keywords, which matches the person's headline, summary, skills and job descriptions. Use q for free text across name, title and company.
Does the data update in real time?
The dataset is refreshed monthly, and each record carries its own fetched_at date. Read that date before you act on a role change.
How many records will a pilot cost?
A pilot of 25 search rows and a few enrichments stays under 50 records. The free plan holds 1,000 once, with no card.
Can an AI agent do the sourcing?
Yes. The hosted MCP server at https://api.leadocean.io/mcp exposes 13 tools, including free counts. See AI recruiting platforms and people data.
Related reading: internal sales tools on signal APIs uses the same pattern. People data enrichment API covers the enrich call in depth.
Size your first talent pool for free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →