POST /v1/people/enrich takes one identifier and returns the person, including the role they hold now. It costs 1 record per hit. A 404 costs nothing.
There is no separate current-role endpoint. The role is the contact_data.contact_current_experiences array on the person record. This page is the developer reference. For the field meanings and coverage, read Current Role. The same lookup is the leadocean_get_lead tool over MCP. Other lookups are on Enrichments.
Quick start (curl)
Create a free account, copy a key with the enrich scope and export it as LEADOCEAN_API_KEY.
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/acme-example"}'Have a person_id from a search row instead? Send that:
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":"1234567"}'GET works too, with the same fields as query parameters. POST /v1/people/enrich is a deprecated alias, so use the path above in new code.
Node.js (fetch)
const res = await fetch("https://api.leadocean.io/v1/people/enrich", {
method: "POST",
headers: {
"x-api-key": process.env.LEADOCEAN_API_KEY,
"content-type": "application/json",
},
body: JSON.stringify({ linkedin_url: "https://www.linkedin.com/in/acme-example" }),
});
if (res.status === 404) {
console.log("Nothing held for this person. Not billed.");
} else if (!res.ok) {
throw new Error(`${res.status} ${(await res.json()).error?.message}`);
} else {
const { data } = await res.json();
const roles = data.contact_data?.contact_current_experiences ?? [];
console.log(roles[0]?.job_title ?? "Record found, no current role held");
}Python (requests)
import os
import requests
r = requests.post(
"https://api.leadocean.io/v1/people/enrich",
json={"linkedin_url": "https://www.linkedin.com/in/acme-example"},
headers={"x-api-key": os.environ["LEADOCEAN_API_KEY"]},
timeout=10,
)
if r.status_code == 404:
print("Nothing held for this person. Not billed.")
else:
r.raise_for_status()
contact = r.json()["data"].get("contact_data") or {}
roles = contact.get("contact_current_experiences") or []
print(roles[0]["job_title"] if roles else "Record found, no current role held")Request
Send exactly one identifier. Two is a 400. The key needs the enrich scope.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
linkedin_url | string (URL) | One of four | A LinkedIn profile URL. Unambiguous, so use it when you have it. |
email | string | One of four | A work email. Returns the person behind it. |
phone | string | One of four | A number in any common format. Returns who it belongs to. |
person_id | string (digits) | One of four | The id at meta.source_ids.person_id on a people-search row. Reaches the one person in five with no LinkedIn URL. |
reveal_email | boolean | No | Default false. Adds every email and phone we hold, at no extra record. Not needed for the role. |
There is no lookup by name and company. Search by company domain first, then send the person_id back.
Response
Response shape from the API reference. Every value below is a placeholder.
{
"success": true,
"data": {
"contact_data": {
"has_email": true,
"has_phone": false,
"email_status": "verified",
"email_type": "work",
"contact_current_experiences": [
{
"job_title": "Head of Sales",
"job_title_details": "Head Of Sales",
"job_seniority": "director",
"job_functions": ["Sales & Business Development"],
"company_name": "Acme",
"company_domain": "acme.example",
"company_industry": "Manufacturing",
"company_employees": { "number_of_employees": 240 }
}
]
}
},
"meta": { "credits": 1, "schema": "leadocean.person.v1" }
}The OpenAPI spec types the experience objects loosely. The keys above are the ones the enrich docs describe, and the start date, end date and current flag are described in prose without key names. Read one real response before you hard-code those.
| Field | Meaning |
|---|---|
contact_current_experiences[] | The current role or roles. Empty means we hold none. |
job_title | The title as written. job_title_details is the normalised form. |
job_seniority, job_functions[] | Seniority and department of the role. |
company_name, company_domain | The current employer. |
contact_data.has_email | Whether we hold an address, answered even without reveal_email. |
meta.credits | Records billed for the call. |
Values we do not hold come back as null. Use the record's still-at-company status and meta.fetched_at to judge how fresh a role is. The dataset is refreshed monthly.
Errors
Every failure is { "success": false, "error": { "message": "...", "details": ... } }. Statuses are from the limits docs, September 2026.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Two identifiers sent, none sent, or a field failed validation. | Read details, send one identifier. |
| 401 | Missing, wrong or revoked key. | Check the x-api-key header. |
| 402 | Free plan: the 1,000 records are spent. They do not reset. | Upgrade. Not billed. |
| 403 | The key lacks the enrich scope, or the account is suspended. | Create a key with the scope. |
| 404 | We hold nothing for this person, or nothing to enrich them by. | Stop. It costs nothing. Try another identifier. |
| 429 | Over 100 requests a second, or 1 a minute on a paid account past its records. | Wait for Retry-After, then retry. Not billed. |
| 503 | The store behind the lookup is unreachable (code: no_source). | Retry with backoff. Never billed. |
Rate limits and cost
One hit is 1 record, with or without reveal_email. A 404, 402, 429 or 503 costs nothing, and a call that returns no role still costs 1 if the person record came back.
The limit is 100 requests per second per key, on every plan. Free is 1,000 records, one-off, no card. Pro is $499 a month, flat, with no per-record price inside fair use. See pricing.
Size first. A people search with count=true and limit=1 returns a free total in meta.total, capped at 100,000.
Bulk
For a list, use POST /v1/exports instead of looping. One export row is 1 record whatever the columns, and the docs call bulk the cheaper way to enrich. A request takes up to 50,000 rows.
Current-role columns include current_job_title, current_company_name, current_company_domain and current_start_date, which come from the search row, so they need only the search scope. current_job_level, current_job_functions and contact_still_at_company_status need enrich as well. Numbered contact columns are email_N_address, email_N_status and phone_N_number, and the CSV returns email_1_address, email_2_address and so on.
curl -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"name": "Heads of sales with current role",
"filters": { "title": ["Head of Sales"], "country": ["US"] },
"limit": 5000,
"columns": ["person_id", "profile_url", "current_job_title",
"current_company_name", "current_company_domain",
"contact_still_at_company_status", "email_N_address"],
"caps": { "emails": 1 }
}'The reply is a 202 with a job id. Poll GET /v1/exports/{id}, then fetch /v1/exports/{id}/download. The Exports page at app.leadocean.io builds the same file.
FAQ
Does a call that finds no current role cost a record?
If we return the person, yes: 1 record. A 404, where we hold nothing for the person, costs nothing.
Can I look up a job title by name and company?
Not on this endpoint. Search by company domain and title, then send the person_id from the row. See Current Role for the search step.
Can an agent call it without an API key?
Yes. The MCP server at https://api.leadocean.io/mcp signs in with OAuth 2.1. The tool is leadocean_get_lead, 1 record per hit. Setup is in Current Role over MCP. For the term, see API.
Get your current job title API key free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →