GET /v2/people/email/personal takes one LinkedIn URL or person id and returns a list of personal addresses, or an empty list. It costs 1 record per call, including a call that returns nothing. A 404 costs nothing.
This page is the developer reference. For what the data is and how it is filtered, read Find a personal email. The same data is reachable over MCP, and all lookups are in the Enrichments hub.
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/personal" \
--data-urlencode "linkedin_url=https://www.linkedin.com/in/jane-doe" \
-H "x-api-key: $LEADOCEAN_API_KEY"Have a person_id from a search row instead? Send it as contact_id:
curl -s "https://api.leadocean.io/v2/people/email/personal?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/personal");
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 person. 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.length ? data.map((a) => a.email) : "Record found, no personal address held");
}Python (requests)
import os
import requests
r = requests.get(
"https://api.leadocean.io/v2/people/email/personal",
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 person. Not billed.")
else:
r.raise_for_status()
emails = [a["email"] for a in r.json()["data"]]
print(emails or "Record found, no personal address held")Request
Send exactly one identifier. Sending both, or neither, is a 400. The key needs the enrich scope.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
linkedin_url | string (URL) | One of the two | A personal profile URL, linkedin.com/in/... |
contact_id | string (digits) | One of the two | The id at meta.source_ids.person_id on a people-search row. Works for 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@example.com", "verified_batch_date": null }
]
}| Field | Meaning |
|---|---|
data | A list of personal addresses. Empty when we hold none. An empty list is a 200, not an error. |
data[].email | A personal address (webmail and similar) we do not know to be bad. |
data[].verified_batch_date | The date of the verification run, YYYY-MM-DD, or null. A whole batch shares one date. |
There is no status field and no meta object. Our verifier has never tested a personal address, so a status would read "untested" almost every time. Addresses marked invalid, spam_trap, abuse, disposable, risky or role are withheld instead. Withheld is not the same as verified: check the rest yourself before you send. 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": ... } }. The 404 is in the API reference. The other statuses 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. Not billed. |
| 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 no record for this person at all. | Stop. It costs nothing. Try the other 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. |
A 404 and an empty data 200 are different facts. The 404 means we hold nothing, so retry with the other identifier. The empty list means the person exists and no personal address qualifies, and a retry will not change it.
Rate limits and cost
One call is 1 record. A 200 with an empty data list still costs 1 record, because the lookup ran. A 404, 400, 401, 402, 403, 429 or 503 costs 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.
Asking whether a person has an email at all is free. A people-search row carries has_email and email_type, and a search with count=true and limit=1 spends nothing.
Bulk
Past a few thousand lookups, use POST /v1/exports instead of looping. One export row is 1 record whatever the columns. An export takes up to 50,000 rows and needs the search scope plus enrich for email columns.
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, work and personal. Read email_N_type to keep the personal ones.
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, personal email",
"filters": { "title": ["VP Sales"], "country": ["US"], "emailType": ["personal"] },
"limit": 5000,
"columns": ["person_id", "profile_url", "email_N_address", "email_N_type", "email_N_verified_date"],
"caps": { "emails": 3 }
}'The reply is a 202 with a job id. Poll GET /v1/exports/{id}, then fetch /v1/exports/{id}/download. Prefer clicks? The Exports page at app.leadocean.io builds the same file.
FAQ
Does a call that finds no personal email cost a record?
Yes. A 200 with an empty data list costs 1 record, because the lookup ran. A 404, where we hold no record for the person, costs nothing. Size a list with a free count=true search first.
Why is there no status field?
We have never verified a personal address, so a status would read "untested" almost every time. The status is applied for you instead: known-bad addresses are withheld. Verify the rest before you send. For a checked address, use the work email API.
Can I find a personal email by name?
No. This endpoint needs a linkedin_url or a contact_id. Search by title and company domain, then send the person_id from the row back as contact_id. Over MCP the agent calls get_lead with reveal_email, see Find Personal Email with MCP.
Get your personal email API key free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →