GET /v2/people/email/work takes one LinkedIn profile URL and returns one work email at the person's current employer, or null. It costs 1 record per call, including a call that returns null. A 404 costs nothing.
This page is the developer reference. For what the email is and how it is checked, read LinkedIn URL to email. All lookups are listed in the Enrichments hub. To go the other way, see the Email to LinkedIn API.
Quick start (curl)
Create a free account, copy a key with the enrich scope and export it as LEADOCEAN_API_KEY.
curl -s -G "https://api.leadocean.io/v2/people/email/work" \
--data-urlencode "linkedin_url=https://www.linkedin.com/in/jane-doe" \
-H "x-api-key: $LEADOCEAN_API_KEY"No profile URL for the person? Send the person_id from a search row as contact_id:
curl -s "https://api.leadocean.io/v2/people/email/work?contact_id=1234567" \
-H "x-api-key: $LEADOCEAN_API_KEY"The endpoint also answers POST with a JSON body of the same fields.
Node.js (fetch)
const url = new URL("https://api.leadocean.io/v2/people/email/work");
url.searchParams.set("linkedin_url", "https://www.linkedin.com/in/jane-doe");
const res = await fetch(url, {
headers: { "x-api-key": process.env.LEADOCEAN_API_KEY },
});
if (res.status === 404) {
console.log("No record for this profile. Not billed.");
} else if (!res.ok) {
throw new Error(`${res.status} ${(await res.json()).error?.message}`);
} else {
const { data } = await res.json();
console.log(data ? `${data.email} (${data.status})` : "Person found, no qualifying email");
}Python (requests)
import os
import requests
r = requests.get(
"https://api.leadocean.io/v2/people/email/work",
params={"linkedin_url": "https://www.linkedin.com/in/jane-doe"},
headers={"x-api-key": os.environ["LEADOCEAN_API_KEY"]},
timeout=10,
)
if r.status_code == 404:
print("No record for this profile. Not billed.")
else:
r.raise_for_status()
data = r.json()["data"]
print(f"{data['email']} ({data['status']})" if data else "Person found, no qualifying email")Request
Send exactly one identifier. Sending both, or neither, is a 400. The key needs the enrich scope, which Free keys have.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
linkedin_url | string (URL) | One of the two | A personal profile URL, linkedin.com/in/... Company pages are not people. |
contact_id | string (digits) | One of the two | The id at meta.source_ids.person_id on a people-search row. Reaches the one person in five with no LinkedIn URL. |
person_id | string (digits) | Alias | The original name of contact_id. Same behaviour. Never send both. |
There is no lookup by name, and this endpoint takes no email or phone as input.
Response
Response shape from the API reference. The address below is a placeholder.
{
"success": true,
"data": {
"email": "jane.doe@acme.example",
"status": "verified",
"verified_batch_date": "2026-09-14"
}
}| Field | Meaning |
|---|---|
data | One address object, or null when no address qualifies. null is a 200, not a 404. |
data.email | The work address, at the domain of the person's current employer. |
data.status | verified, catch_all_valid or catch_all. Nothing else is returned here. |
data.verified_batch_date | Date of the verification run, YYYY-MM-DD. null means the check was not ours. It does not mean unverified. |
There is no found field and no meta object. A former employer's address is never returned, and neither is an address nobody has tested. So null means "none that qualify", not "no email exists". Quota state rides in the X-Quota-State, X-Quota-Reset and X-RateLimit-Limit headers.
Errors
Every failure is { "success": false, "error": { "message": "...", "details": ... } }. Status codes are from the limits docs, September 2026.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Both identifiers sent, neither 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. |
| 403 | The key lacks the enrich scope, or the account is suspended. | Create a key with the scope. |
| 404 | We hold no record for this profile at all. | Stop. It costs nothing. Check the URL, or search for the person and use contact_id. |
| 429 | Over 100 requests a second, or 1 a minute on a paid account past its records. | Wait for Retry-After, then retry. |
| 503 | The store behind the lookup is unreachable (code: no_source). | Retry with backoff. |
Rate limits and cost
What a call costs depends on how it ends.
| Outcome | Status | Cost |
|---|---|---|
| Email returned | 200 | 1 record |
Person found, no qualifying email (data: null) | 200 | 1 record |
| No record for the profile | 404 | Nothing |
| Rejected request | 400, 401, 402, 403, 429, 503 | Nothing |
The call does not report its own price. Read GET /v1/usage, which is free. 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 before you spend. A people search with count=true and limit=1 is free and returns the total in meta.total, capped at 100,000. Each search row carries has_email and email_status, so you can skip people who hold no address.
Bulk
The docs describe no batch version of this endpoint, so a list of LinkedIn URLs means one call per URL. Stay under 100 requests per second and honour Retry-After on a 429.
If you want people matching a filter rather than a list you already hold, use POST /v1/exports. One export row is 1 record whatever the columns, up to 50,000 rows, and it needs the search scope plus enrich for email columns. It takes filters, not a list of URLs.
Email columns are numbered: email_N_address, email_N_type, email_N_status and email_N_verified_date come back as email_1_address, email_2_address and so on. caps sets how many per person (default 3). An export lists every address we hold, so read email_N_type and email_N_status to pick the work one.
curl -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"name": "VP Sales, work email verified",
"filters": { "title": ["VP Sales"], "country": ["US"], "emailType": ["work"], "emailStatus": ["verified"] },
"limit": 5000,
"columns": ["person_id", "profile_url", "email_N_address", "email_N_type", "email_N_status"]
}'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 LinkedIn URL that returns null cost a record?
Yes. A 200 with data: null costs 1 record, because the lookup ran. A 404, where we hold no record for the profile, costs nothing. The person exists but no address passed the status rule, so a retry will not change it.
Can I run it from an AI agent without a key?
Yes. The MCP server at https://api.leadocean.io/mcp signs in with OAuth 2.1, so no key is pasted. There is no separate work email tool: use leadocean_get_lead with linkedin_url and reveal_email set to true, 1 record. A miss there costs nothing. Setup is in LinkedIn to Email over MCP.
Can I go from an email back to the LinkedIn profile?
Yes. GET /v2/people/reverse takes an email and returns the person and their profile URL. Code is in the Email to LinkedIn API, and the overview is email to LinkedIn.
Get your LinkedIn to email API key free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →