A job change signal is a person you already know starting a new role at a new company. A former champion or buyer arrives at the new job already knowing your product. This guide builds the detector with LeadOcean, using only documented endpoints.
One thing first. LeadOcean's filter list has no "changed jobs in the last 30 days" filter, and this guide does not pretend otherwise. You snapshot people, re-enrich them on a schedule, and diff the current employer.
Examples use placeholders (Acme, acme.com, Jane Doe).
TL;DR
- Search once to build a watchlist, store each
person_id, then re-enrich monthly and compare the current employer. - Takes about 30 minutes to set up. The snapshot spends 100 records, and each monthly check of 100 people spends up to 100 more.
- The Free plan (1,000 records for life) covers the snapshot and several checks. Past that, Solo at $97 a month or Agency at $497 a month has a fair-use pace instead of credits.
Prerequisites
- A LeadOcean API key in
$LEADOCEAN_API_KEY(free at app.leadocean.io, no card). curlandjq.- A cron job or any scheduler. Nothing else.
Step 1: Size the watchlist for free
Pick the people whose moves matter: past champions, closed-won buyers, or a target title list. Size it before you spend anything.
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": ["VP", "Director"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"employeeRange": ["51-200"],
"limit": 1
}'count=true is a query parameter, not a body field. It returns meta.total (capped at 100,000) and an empty data array, and it spends no records.
Step 2: Snapshot the watchlist
Run the same body without count, with limit at 100. Save each row's ID from meta.source_ids.person_id.
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","Director"],"jobFunction":["Sales & Business Development"],
"country":["US"],"employeeRange":["51-200"],"limit":100}' \
| jq -r '.data[].meta.source_ids.person_id' > watchlist.txtThis spends 100 records, one per person returned. For a list you already own, skip search. Enrich by linkedin_url or email instead.
Step 3: Re-enrich and store the current employer
POST /v1/people/enrich takes exactly one of linkedin_url, email, phone or person_id. The current role is in contact_data.contact_current_experiences[], with company_name, company_domain and job_title. Past roles are in resume_data.experiences[] (since October 2026).
while read -r id; do
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\":\"$id\"}" \
| jq -c --arg id "$id" '{id: $id,
company: (.data.contact_data.contact_current_experiences[0].company_domain // null),
title: (.data.contact_data.contact_current_experiences[0].job_title // null)}'
sleep 0.25
done < watchlist.txt > snapshot_$(date +%F).jsonlThe sleep keeps you under 5 requests a second on Free. A person we hold nothing for is a 404 and costs nothing.
Step 4: Diff against last month
Compare the two snapshots. A changed current employer domain is a job change signal.
join -j1 \
<(jq -r 'select(.company) | [.id, .company] | @tsv' snapshot_2026-09-21.jsonl | sort) \
<(jq -r 'select(.company) | [.id, .company] | @tsv' snapshot_2026-10-21.jsonl | sort) \
| awk '$2 != $3 {print $1}' > movers.txtComparing the domain, not the whole role, keeps a retitle at the same company out of the list. Diff title separately if promotions matter to you. Rows with no current role are skipped, so a 404 never reads as a move.
Step 5: Route the movers
For each mover, enrich again with reveal_email=true for the new work address, then branch on where they landed.
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","reveal_email":true}'Check contact_data.email_status before sending, because bounced emails are not refunded. Then size the new employer for free with GET /v1/companies/count, or ask the MCP tool leadocean_count_companies in Claude. If the new company fits your ICP, open a thread that references the shared history.
The work_other value of emailType marks an address at a former employer. Use it to spot stale emails in an old list.
What you get
The Step 1 filters, sized on 2026-10-21 with the free leadocean_count_leads MCP tool:
{"total": 55020, "totalIsExact": true, "totalAll": 180244, "distinctCompanies": 28098, "credits": 0}total counts people with a mailable email (verified, catch_all_valid or catch_all), the tool's default. totalAll is the same filters with no email condition.
A re-enriched mover comes back in the shape below. This is the response shape from the API reference, with placeholder values. The record is not a real person.
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"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", "start_date": "2026-09-01" }
]
},
"resume_data": {
"experiences": [
{ "company_name": "Example Corp", "job_title": "Director of Sales", "start_date": "2021-02-01", "end_date": "2026-08-31" }
]
}
},
"meta": { "credits": 1 }
}The keys follow the person schema and the MCP docs. Past employers carry no company_id yet, so match them by name.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported and the header name is exact. |
| 429 | Above the plan rate limit (5 a second on Free, 200 on Agency) | Add sleep, or honour Retry-After. |
| 402 | The 1,000 free records are spent | Move to Solo ($97 a month) or Agency ($497 a month). |
| 404 on enrich | We hold nothing for that person | Free. Drop them from the watchlist. |
Empty data from search | An enum value is wrong, such as vp for jobLevel | Use exact strings like VP. List them with GET /v1/enums. |
| Search stops early | One search pages to 10,000 rows (meta.depthCapped) | Narrow the filters instead of walking the index. |
FAQ
Is there a filter for people who changed jobs recently?
No. The filter list does not include one. The re-enrich and diff method above is how you detect a change with LeadOcean.
How often should I check?
Monthly. The dataset is refreshed monthly, and each record carries its own fetched_at date. Checking weekly or daily mostly re-reads unchanged data and spends records.
How much does it cost to monitor 500 people?
Up to 500 records per check. Misses cost nothing. Free's 1,000 covers a 500-person snapshot and one check. Solo and Agency run on a fair-use pace, not per-record credits.
Can I run this from Claude instead of a script?
Yes, through the hosted MCP server at https://api.leadocean.io/mcp. It has 13 tools, including leadocean_count_leads and leadocean_count_companies, both free.
What do I do with a mover?
Reach out within days, name the shared history, and skip the pitch. See warm outbound, job change rates by function and using intent signals for outbound. Plans are on pricing.
Build your job change detector on 1,000 free records
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →