A candidate enrichment API should return nine layers. They are identity, current role, career history, education and credentials, skills, work email, phone, employer firmographics and freshness. This guide gives you one call per layer, so you can test any API in 20 minutes before you build on it. We run the calls against LeadOcean because that is what we can show.
Most checklists stop at "contact data and job history". That misses the layers that decide whether you can trust and act on a record.
TL;DR
- Check nine layers with two enrich calls and one free count. Each layer has a field to look for and a failure to watch for.
- Time: about 20 minutes. Records: 2 at most (one person, one company). A count, a 404 and an empty page cost 0.
- A layer that comes back empty is fine if the API says so. A layer that comes back wrong with no date is the real problem.
Prerequisites
- A LeadOcean API key in
$LEADOCEAN_API_KEY. The Free plan gives 1,000 records for life, no card (sign up). curlandjq.- One candidate you know well, as a LinkedIn URL or email. Test on yourself or a colleague who agrees. The examples use placeholders:
https://www.linkedin.com/in/janedoe-exampleandjane.doe@acme.com.
Step 1: Size your candidate pool for free
Count first, so you know the API covers the people you hire. A count spends 0 records. Send count=true as a query parameter, not in the JSON body, with limit 1. meta.total is capped at 100,000.
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": ["Software Engineer"],
"keywords": ["Kubernetes"],
"country": ["GB"],
"limit": 1
}' | jq '.meta'Filters are arrays of strings and enum values are exact. Check a value with GET /v1/enums or the MCP tool leadocean_list_enum_values first.
Step 2: Pull one person and check layers 1 to 5
One call returns the first five layers. Send exactly one identifier: linkedin_url, email, phone or person_id. Set reveal_email to true to get addresses back. It costs no extra record.
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/janedoe-example", "reveal_email": true}' \
> person.jsonNow read each layer from the file.
| Layer | What to look for | Where it is |
|---|---|---|
| 1. Identity | Name, headline, location | data.profile_data |
| 2. Current role | Employer, title, start date | data.contact_data.contact_current_experiences[] |
| 3. Career history | Past roles with dates and job level | data.resume_data.experiences[] |
| 4. Education and credentials | Schools, certifications, awards | educations[], certifications[], awards[] in resume_data |
| 5. Skills | Listed skills, publications, patents | r_skills[], publications[], patents[] in resume_data |
jq '{
identity: .data.profile_data,
current: .data.contact_data.contact_current_experiences,
past_roles: (.data.resume_data.experiences | length),
education: (.data.resume_data.educations | length),
certifications: (.data.resume_data.certifications | length)
}' person.jsonCareer history arrived in October 2026. Every array is present only when held, so an empty one means no data, not an error. Past employers carry no company_id yet, so match them by name. The current role is not repeated in experiences.
Step 3: What each layer is for
Do not buy a layer you will not use. Here is what each one does in a recruiting workflow.
- Identity de-duplicates. Two ATS rows with the same name and headline are one person.
- Current role is the trigger. A changed employer or title is the reason to reach out now.
- Career history personalises the message and shows tenure. Treat it as context, not a screening score.
- Education and credentials matter for regulated roles, where a certification is a hard requirement.
- Skills are the weakest layer. Self-listed skills are unverified, so use them to rank, not to reject.
- Work email and phone are the channels. Without a status on each, you are guessing.
- Employer firmographics let you filter by company size, industry and stack.
- Freshness tells you when to re-enrich.
A quick way to see which layers a record fills is to count the non-empty parts.
jq '{current: (.data.contact_data.contact_current_experiences | length),
history: (.data.resume_data.experiences | length),
education: (.data.resume_data.educations | length),
email: (.data.contact_data.has_email),
phone: (.data.contact_data.has_phone)}' person.jsonRun it on ten candidates you know and you will see the fill pattern for your own roles. Engineers and recruiters you know well are a better test than random records.
If an API cannot return layers 2, 6 and 9, it is a lookup tool, not enrichment. Everything else is a bonus you weigh by role type.
Step 4: Check the contact layers (6 and 7)
Contact data is where enrichment APIs differ most. Layer 6 is a work email with a deliverability verdict. Layer 7 is a phone number with a type.
jq '.data.contact_data | {
has_email, has_phone, email_status, email_type,
emails: [.contact_emails[]? | {email, type, status, verified_batch_date}],
phones: [.contact_phones[]? | {phone, type}]
}' person.jsonRead three things before you trust an address.
email_statusis the verdict. Mail onlyverifiedorcatch_all_valid. The full list has 13 values, includingrisky,unknownandinvalid.email_typetells you whose address it is.workis the current employer.work_otheris a former employer and the likeliest to bounce.has_phoneanswers even when you did not ask for numbers, so you can see one exists before you spend on it.
For a phone alone, call GET /v2/people/phone. Those v2 contact endpoints cost one record per call, including a call that finds nothing. We give no refund for bounced emails, so read the status first.
Step 5: Check the employer layer (8)
A candidate record is half a company record. Ask for the employer's size, industry and detected technology, because recruiters filter on them. Take the domain from layer 2.
curl -s -X POST https://api.leadocean.io/v1/companies/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "acme.com"}' \
| jq '.data | {company: .company_data, tech: .company_detected_technologies, metrics: .company_metrics}'The response has three parts: company_data, company_detected_technologies and company_metrics. Technology covers about 7.8M crawled company sites, so small employers often return none. Sizing a company list is free with GET /v1/companies/count or the MCP tool leadocean_count_companies.
Step 6: Check freshness and source (layer 9)
Layer 9 is the one most lists forget. A field without a date is a guess. Look for a fetch date on the record and a verification date on each email.
jq '{fetched_at: .data.meta.fetched_at,
email_verified: [.data.contact_data.contact_emails[]? | .verified_batch_date]}' person.jsonThe dataset refreshes monthly, and each record carries its own fetched_at. A verified_batch_date of null means our verifier did not run the check itself, so treat it as a signal, not a gap. Store both dates next to the data. They let you skip records you enriched recently.
Step 7: Score the API against the nine layers
Write the result down. A simple pass table keeps the comparison honest when you test a second API.
layer present dated note
1 identity yes yes
2 current role yes yes
3 career history yes yes past employers have no company_id
4 education/creds partial yes arrays only when held
5 skills partial yes
6 work email yes yes check email_status
7 phone partial no dnc is null, never "safe to call"
8 employer yes yes
9 freshness yes n/a monthly refreshFill in your own results. The "partial" rows are normal: no database holds everything for everyone. Weigh a missing layer against the cost of getting it elsewhere.
What you get
Sizing first. The MCP tool leadocean_count_leads ran on 2026-10-21 and spent 0 records:
title: ["Software Engineer"], keywords: ["Kubernetes"], country: ["GB"]
-> total 3,993 (mailable), totalAll 9,980, distinctCompanies 1,508The 3,993 counts only verified, catch_all_valid and catch_all addresses, which is the tool's default. The 9,980 is everyone matching the same filters with no email condition.
The enrich response (response shape from the API reference and the enrich docs, placeholder values only).
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "Platform Engineer at Acme" },
"contact_data": {
"has_email": true,
"has_phone": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{ "email": "jane.doe@acme.com", "type": "work", "status": "verified", "verified_batch_date": "2026-09-20" }
],
"contact_phones": [{ "phone": "+14155550133", "type": "mobile" }],
"contact_current_experiences": [
{ "company_name": "Acme", "company_domain": "acme.com", "job_title": "Platform Engineer" }
]
},
"resume_data": {
"experiences": [{ "company_name": "Globex", "job_title": "DevOps Engineer" }],
"educations": [],
"certifications": []
},
"meta": { "schema": "leadocean.person.v1", "fetched_at": "2026-10-01" }
},
"meta": { "credits": 1, "source": "own" }
}Two things to notice in the shape. The email carries its own status and verification date, so you never trust an address blind. Arrays such as educations are present and empty when nothing is held, which your parser must accept.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked key | Check the x-api-key header and that $LEADOCEAN_API_KEY is set in this shell. |
| 400 on enrich | More than one identifier sent | Send exactly one of linkedin_url, email, phone or person_id. |
| 404 on enrich | Nobody matched | Free. Try another identifier, or search by domain and enrich by person_id. |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Wait for Retry-After, then retry. |
| 402 | The 1,000 Free records are spent | They do not reset. Move to a paid plan (pricing). |
Empty search data | A filter value is not an exact enum string | Check values with GET /v1/enums or leadocean_list_enum_values. |
FAQ
What is the minimum data a candidate enrichment API should return?
Current employer and title, a verified work email with a status, and a fetch date. Those three let you route, contact and trust a record. Career history, education and skills are what make the outreach specific.
Does LeadOcean return skills and certifications?
It returns them in resume_data when held, since 2026-10-17. Each array can be empty for a given person. Test a few of your own candidates before you plan around them.
Why does a fetch date matter more than another field?
People change jobs, so any stale field misleads you. With a date on every record you can decide what to re-enrich. The dataset refreshes monthly.
How many records does the audit cost?
Two at most: one person enrich and one company enrich. Sizing counts, 404s and misses cost nothing. Free covers this many times over.
Where do I go from here?
To enrich a whole ATS, follow how to enrich candidate records. To pick a vendor, see best candidate enrichment APIs for recruiting platform builders. For the basics, read what is data enrichment and data enrichment for developers. Plans start free; see pricing.
Test all nine layers on your own candidates, free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →