TL;DR
- Use LeadOcean for who the person is (employer, past roles,
publications[]). Use a web search API for what they published or said in public. - It takes about 30 minutes to wire up. LeadOcean spends 1 record per candidate enriched. A miss costs nothing.
- The step that matters is matching. Keep a web result only when its employer or co-author agrees with the LeadOcean record.
LeadOcean does not search the open web. Say that first, because it decides the design. A people API holds the profile. A web search API reaches papers, conference talks and blog posts. You need both, and a check in the middle.
The Crustdata post on this topic covers the search half well. Its web search guide (October 2026) shows one endpoint, POST https://api.crustdata.com/web/search/live. It bills one credit per query and takes sources for scholar articles, scholar authors and social posts. This guide is the shorter version, with the LeadOcean record as the identity anchor.
Prerequisites
- A LeadOcean API key in
$LEADOCEAN_API_KEY. The free plan works: 1,000 records for life, no card, 5 requests a second. - A web search API key. The examples use Crustdata's, in
$SEARCH_API_KEY. Any search API that returns a title, URL and snippet fits the same loop. curlandjq, or Python 3 withrequests.- A candidate you can identify by
linkedin_url,emailorperson_id.
Step 1: Enrich the candidate and read what is already on record
Start with LeadOcean. The enrich call returns the current role and, since October 2026, career history in resume_data. That includes publications[], patents[], memberships[] and awards[] when held. Past employers carry 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 '{"linkedin_url": "https://www.linkedin.com/in/jane-doe-example"}' \
| tee candidate.json \
| jq '{name: .data.profile_data.profile_full_name,
employer: .data.contact_data.contact_current_experiences[0].company_name,
publications: .data.resume_data.publications}'Exactly one of linkedin_url, email, phone or person_id goes in the body. There is no name plus domain lookup. If you only have a name and a company, search by domain first, then enrich by person_id.
Step 2: Build the search queries from the record
Never search on the name alone. A common name returns a different person's paper. Use the employer, a past employer, a school or a topic from the record as a second term.
import json
c = json.load(open("candidate.json"))["data"]
name = c["profile_data"]["profile_full_name"]
employer = c["contact_data"]["contact_current_experiences"][0]["company_name"]
school = (c.get("resume_data", {}).get("educations") or [{}])[0].get("school_name", "")
queries = {
"papers": {"query": f'"{name}" {employer}', "sources": ["scholar-articles"]},
"author": {"query": name, "sources": ["scholar-author"]},
"talks": {"query": f'"{name}" {employer} talk', "sources": ["web"], "site": "youtube.com"},
"posts": {"query": f'"{name}" {employer}', "sources": ["social"]},
}The keys inside educations[] and publications[] are not pinned in the API reference, so read them on your own response before you rely on school_name or title. Print candidate.json once and adjust.
Step 3: Run the web search
One query per source. The request below is the shape shown in Crustdata's own guide (October 2026).
curl -s -X POST https://api.crustdata.com/web/search/live \
-H "authorization: Bearer $SEARCH_API_KEY" \
-H "content-type: application/json" \
-H "x-api-version: 2025-11-01" \
-d '{"query": "\"Jane Doe\" Acme", "sources": ["scholar-articles"]}'Per that guide, scholar results carry a title, a link, a citation count and an authors array, and the site parameter works on web and news results. Web and social results carry a title, URL and snippet. Check the vendor docs for the current response fields before you parse them.
Four queries per candidate is four search credits. Run the talks and posts queries only for candidates who pass your first screen.
Step 4: Confirm it is the right person
Accept a result only if it passes at least one of these tests against the LeadOcean record:
- The page names the same employer, or a past employer from
experiences[]. - The author's affiliation matches a school in
educations[]. - A title in
publications[]appears in the scholar result.
def same_person(result_text: str, record: dict) -> bool:
text = result_text.lower()
employers = {e["company_name"].lower()
for e in record.get("resume_data", {}).get("experiences", [])
if e.get("company_name")}
titles = {p["title"].lower()
for p in record.get("resume_data", {}).get("publications", [])
if p.get("title")}
return any(e in text for e in employers) or any(t in text for t in titles)Anything that fails goes to a review pile, not into the record. A wrong paper on a candidate's file is worse than a missing one.
Step 5: Write the structured record
Store the link, the source and the date. Do not store the page text. You need the claim and the pointer back to it.
{
"person_id": "pid_example",
"enriched_at": "2026-10-21",
"signals": [
{"type": "paper", "title": "Example paper title", "url": "https://example.org/paper", "matched_on": "employer"},
{"type": "talk", "title": "Example talk", "url": "https://example.org/talk", "matched_on": "publications[]"}
]
}What you get
Sizing first. The free leadocean_count_leads tool on 2026-10-21, with title: Research Scientist, keywords: machine learning, country: US:
{
"total": 5908,
"totalIsExact": true,
"totalAll": 11693,
"distinctCompanies": 1322,
"credits": 0
}The total counts people with a verified or catch-all email (the tool default). totalAll is the same filters with no email condition. The count spent no records.
Next, the enrich response. This is the response shape from the API reference, with placeholder values:
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"contact_current_experiences": [{ "company_name": "Acme" }]
},
"resume_data": {
"experiences": [],
"educations": [],
"publications": [],
"patents": []
},
"meta": { "fetched_at": "2026-10-21" }
}
}Sections appear only when held. An empty publications means we hold none, not that the person has none.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-export $LEADOCEAN_API_KEY and check the header name |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After and sleep between candidates |
| 402 | The 1,000 free records are spent | Move to a paid plan on the pricing page |
| 404 on enrich | Nobody matched that input | Count it as a miss. It costs nothing |
| Empty search results | A bad enum value such as Director of Engineering for jobLevel | Use Director and exact values from leadocean_list_enum_values |
| Search stops early | One search pages to 10,000 rows (meta.depthCapped) | Narrow the filters instead of walking the index |
FAQ
Does LeadOcean have a web search API?
No. LeadOcean returns people and company data: profile, contacts and career history. Papers, talks and posts come from a separate web search API, then you match them back to the LeadOcean record.
How many records does this spend?
One per candidate enriched. A 404 costs nothing. The web search credits are billed by the search vendor, not by LeadOcean.
Can I skip the web search and use publications[] only?
Yes, for a first pass. It is free of extra cost once you enrich, but it only holds what we have on record. Use the web search for candidates who pass your screen.
Can an AI agent run this loop?
Yes. LeadOcean has a hosted MCP server at https://api.leadocean.io/mcp with 13 tools, including free sizing with leadocean_count_leads. Pair it with whatever search tool your agent already has.
For the GitHub side of the same job, see enrich a candidate from their GitHub. To compare vendors, read best people search APIs and the company search API guide. All guides live in the blog.
Enrich your first candidate free, then add the papers
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →