Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Identify Candidates Open to Change Before They Signal It Publicly (With a Search and Enrich API)

Search a cohort, enrich each person for career history, and rank by how far current tenure has run past their own pattern.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

You cannot read intent from a profile, but you can rank employed people by one proxy for a coming move: how long they have stayed compared with their own history. This guide does that with two LeadOcean calls and a short script. It gives you a ranked list, not a verdict.

TL;DR

  • Search a cohort, enrich each person for resume_data, then score current tenure against their average past tenure.
  • It takes about 30 minutes and spends 1 record per person returned by search and 1 per person enriched. A 25-person pilot is 50 records, inside the Free plan's 1,000.
  • The score is a heuristic. Use it to order outreach, never to claim someone is looking.

Prerequisites

  • A LeadOcean API key from app.leadocean.io, exported as $LEADOCEAN_API_KEY. Free works: 1,000 records for life, no card, 5 requests a second.
  • curl and Python 3.
  • A role you recruit for. The examples use data engineers at mid-size companies in the US.

Step 1: Size the cohort for free

Count first. A count spends nothing, and a cohort over 10,000 rows cannot be paged in one search (meta.depthCapped), so narrow it before you pull anything.

bash
curl -s "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "title": ["Data Engineer"],
    "country": ["US"],
    "jobLevel": ["Staff"],
    "employeeRange": ["1001-5000"]
  }'

count=true goes in the query string, not the body. meta.total is capped at 100,000. In the MCP server the same check is leadocean_count_leads, which is also free. Enum values are exact strings, so check GET /v1/enums or leadocean_list_enum_values before you guess.

Step 2: Pull a page of people

Search returns the current role and start date on each row. Each person returned is one record.

bash
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": ["Data Engineer"],
    "country": ["US"],
    "jobLevel": ["Staff"],
    "employeeRange": ["1001-5000"],
    "limit": 25
  }' > search.json

The body is flat JSON and every filter is an array. limit runs 1 to 100. Page on with meta.nextCursor. Each row carries meta.source_ids.person_id; keep it, because about one person in five has no LinkedIn URL and that id is the only way to enrich them.

Step 3: Enrich for career history

Enrichment returns resume_data, which holds past roles in experiences[]: job_title, company_name, start_date, end_date and job_seniority.job_level. It has been in the response since 2026-10-17. Past employers come back by name only, with no company_id.

bash
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"}' > person.json

Send exactly one of linkedin_url, email, phone or person_id. A person we hold nothing for is a 404 and costs nothing. Leave reveal_email off here: it is not needed to score, and you can reveal contact data later for only the people you shortlist.

Step 4: Score tenure against their own pattern

The signal is relative. Take someone who stayed about 2.5 years in each of three roles and is now 3.4 years into a fourth. They are further past their pattern than someone at 3.4 years who has always stayed 8. Compute that ratio.

The current role sits in contact_data.contact_current_experiences with its start_date. Past roles are in resume_data.experiences, and the current role is not repeated there.

python
import json, sys
from datetime import date

START, END = "start_date", "end_date"

def years(a, b):
    return (b - a).days / 365.25

def parse(s):
    return date.fromisoformat(s[:10]) if s else None

def score(person):
    data = person["data"]
    now = data["contact_data"]["contact_current_experiences"][0]
    cur_start = parse(now.get(START))
    if not cur_start:
        return None
    past = []
    for e in data.get("resume_data", {}).get("experiences", []):
        a, b = parse(e.get(START)), parse(e.get(END))
        if a and b and b > a:
            past.append(years(a, b))
    if len(past) < 2:
        return None          # too little history to compare
    avg = sum(past) / len(past)
    tenure = years(cur_start, date.today())
    return round(tenure / avg, 2), round(tenure, 1), round(avg, 1)

p = json.load(open(sys.argv[1]))
print(score(p))

A ratio near or above 1.0 means the person is at or past their usual stay. Under 0.5 they have only just arrived. Requiring two past roles stops one short job from distorting the average.

Step 5: Rank, then add one more check

Run the script over every enriched file and sort descending by ratio. Then drop anyone whose still-at-company status on the record suggests the role is stale, since the dataset refreshes monthly and a record can lag a real move by weeks.

Two further signals fit the same data. A title that has not risen across the last two employers is a lateral pattern. A long tenure with no level change shows as an unchanged job_seniority.job_level across resume_data.experiences. Add them as extra columns, not as extra weight, until you have checked them against replies from your own outreach.

What you get

Response shape from the API reference, with placeholder values. This is not a real person.

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe" },
    "contact_data": {
      "has_email": true,
      "email_status": "verified",
      "contact_current_experiences": [
        { "job_title": "Staff Data Engineer", "company_name": "Acme",
          "company_domain": "acme.com", "start_date": "2023-04-01" }
      ]
    },
    "resume_data": {
      "experiences": [
        { "job_title": "Data Engineer", "company_name": "Example Corp",
          "start_date": "2019-06-01", "end_date": "2021-09-01" },
        { "job_title": "Senior Data Engineer", "company_name": "Sample Ltd",
          "start_date": "2021-10-01", "end_date": "2023-03-01" }
      ]
    }
  },
  "meta": { "credits": 1 }
}

For this placeholder record the script prints a tuple of three numbers: the ratio, current tenure in years and average past tenure in years. Field names follow the enrichment docs.

The Step 1 cohort, counted with the free leadocean_count_leads tool on 2026-10-21:

text
filters: title "Data Engineer", country US, jobLevel Staff, employeeRange 1001-5000
total (no email condition):                     12,454
mailable (verified, catch_all_valid, catch_all): 5,254
distinct employers:                              1,941

At 12,454 the cohort is over the 10,000-row depth of one search. Split it, for example by city or a narrower title, before you page through it.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported in this shell
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honour Retry-After; enrich in a slow loop
402Out of recordsCheck GET /v1/account; the Free 1,000 is one-off
Zero rowsA filter value is not an exact enum stringList values with GET /v1/enums
404 on enrichNo record we can enrich by that idCosts nothing; skip the person
Search stops, depthCapped trueOne search pages to 10,000 rowsNarrow filters and run several searches

FAQ

Is this the same as an open-to-work signal?

No. It is an inference from history, and it can be wrong. Treat a high ratio as a reason to write first, not as proof.

How many records does a pilot cost?

A page of 25 people is 25 records. Enriching all 25 is 25 more, so 50. Misses on enrich cost nothing.

Can I filter by tenure directly?

Not as a filter in the documented search parameters. Pull the cohort, enrich, then compute tenure yourself as above.

Can I run this from an AI agent?

Yes. The MCP server at https://api.leadocean.io/mcp exposes 13 tools, including leadocean_get_lead for enrichment and leadocean_count_leads for free sizing.

Does this replace a recruiting CRM?

No. LeadOcean returns data. It has no sequencer, dialer or CRM.

Related reading: how to identify and act on buying signals, job change signals, buying signal and pricing.

Rank your first 25 candidates by tenure signal, free

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →