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

How-to guide

How to Audit a Candidate Enrichment API (9 Data Layers)

Nine layers of data to demand from a candidate enrichment API, the call that proves each one, and what it costs in records.

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

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).
  • curl and jq.
  • 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-example and jane.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.

bash
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.

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 '{"linkedin_url": "https://www.linkedin.com/in/janedoe-example", "reveal_email": true}' \
  > person.json

Now read each layer from the file.

LayerWhat to look forWhere it is
1. IdentityName, headline, locationdata.profile_data
2. Current roleEmployer, title, start datedata.contact_data.contact_current_experiences[]
3. Career historyPast roles with dates and job leveldata.resume_data.experiences[]
4. Education and credentialsSchools, certifications, awardseducations[], certifications[], awards[] in resume_data
5. SkillsListed skills, publications, patentsr_skills[], publications[], patents[] in resume_data
bash
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.json

Career 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.

bash
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.json

Run 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.

bash
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.json

Read three things before you trust an address.

  • email_status is the verdict. Mail only verified or catch_all_valid. The full list has 13 values, including risky, unknown and invalid.
  • email_type tells you whose address it is. work is the current employer. work_other is a former employer and the likeliest to bounce.
  • has_phone answers 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.

bash
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.

bash
jq '{fetched_at: .data.meta.fetched_at,
     email_verified: [.data.contact_data.contact_emails[]? | .verified_batch_date]}' person.json

The 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.

text
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 refresh

Fill 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:

text
title: ["Software Engineer"], keywords: ["Kubernetes"], country: ["GB"]
-> total 3,993 (mailable), totalAll 9,980, distinctCompanies 1,508

The 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).

json
{
  "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

ErrorCauseFix
401Missing, wrong or revoked keyCheck the x-api-key header and that $LEADOCEAN_API_KEY is set in this shell.
400 on enrichMore than one identifier sentSend exactly one of linkedin_url, email, phone or person_id.
404 on enrichNobody matchedFree. Try another identifier, or search by domain and enrich by person_id.
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Wait for Retry-After, then retry.
402The 1,000 Free records are spentThey do not reset. Move to a paid plan (pricing).
Empty search dataA filter value is not an exact enum stringCheck 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 →