Enrichment API

Find Phone Number API

GET /v2/people/phone. One LinkedIn URL or person id in, every number we hold out, best first. 1 record per call, a 404 is free.

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

GET /v2/people/phone takes one LinkedIn URL or person id and returns every number we hold, best first. It costs 1 record per call, including a call that finds nothing. A 404 costs nothing.

This page is the developer reference. For what the data is and where it comes from, read Find a phone number. The same lookup is the leadocean_find_phone tool over MCP.

Quick start (curl)

Create a free account, copy a key with the enrich scope and export it as LEADOCEAN_API_KEY.

bash
curl -s -G "https://api.leadocean.io/v2/people/phone" \
  --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:

bash
curl -s "https://api.leadocean.io/v2/people/phone?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)

js
const url = new URL("https://api.leadocean.io/v2/people/phone");
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.found ? data.best : "Record found, no number held");
}

Python (requests)

python
import os
import requests

r = requests.get(
    "https://api.leadocean.io/v2/people/phone",
    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()
    data = r.json()["data"]
    print(data["best"] if data["found"] else "Record found, no number held")

Request

Send exactly one identifier. Sending both, or neither, is a 400. The key needs the enrich scope.

ParameterTypeRequiredMeaning
linkedin_urlstring (URL)One of the twoA personal profile URL, linkedin.com/in/...
contact_idstring (digits)One of the twoThe id at meta.source_ids.person_id on a people-search row. Works for the one person in five with no LinkedIn URL.
person_idstring (digits)AliasThe 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. The older GET /v1/people/phone and the MCP tool also accept a work email or a number you already hold.

Response

Response shape from the API reference. The number below is a placeholder.

json
{
  "success": true,
  "data": {
    "person_id": "1234567",
    "found": true,
    "phones": [
      { "phone": "+14155550133", "type": "mobile" }
    ],
    "best": { "phone": "+14155550133", "type": "mobile" }
  }
}
FieldMeaning
data.foundtrue when we hold at least one number. false is a 200, not an error.
data.phones[]Every number, best first. The array position is the priority.
data.phones[].typeThe source's own label, such as mobile, direct or office. Not a closed list: treat an unknown value as unknown.
data.bestThe first entry of phones, or null.
data.person_idThe id to send back as contact_id. Present even when found is false.

There is no meta object and no dnc field. Quota state rides in the X-Quota-State and X-RateLimit-Limit headers. Absence of dnc is not clearance, so run your own do-not-call check.

Errors

Every failure is { "success": false, "error": { "message": "...", "details": ... } }. Status codes are from the limits docs, September 2026.

StatusMeaningWhat to do
400Both identifiers sent, neither sent, or a field failed validation.Read details, send one identifier. Not billed.
401Missing, wrong or revoked key.Check the x-api-key header.
402Free plan: the 1,000 records are spent. They do not reset.Upgrade. Not billed.
403The key lacks the enrich scope, or the account is suspended.Create a key with the scope.
404We hold no record for this person.Stop. It costs nothing. Try the other identifier.
429Over 100 requests a second, or 1 a minute on a paid account past its records.Wait for Retry-After, then retry. Not billed.
503The store behind the lookup is unreachable (code: no_source).Retry with backoff. Never billed.

Rate limits and cost

One call is 1 record. found: false still costs 1 record. A 404, 400, 401, 402, 403, 429 or 503 costs nothing.

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 hasPhone=true, count=true and limit=1 returns a free total in meta.total, capped at 100,000. Then call this endpoint only for people who hold a number.

Bulk

Past a few thousand lookups, 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. An export takes up to 50,000 rows and needs the search scope plus enrich for phone columns.

Phone columns are numbered: phone_N_number and phone_N_type come back as phone_1_number, phone_1_type, phone_2_number and so on. caps sets how many (default 2).

bash
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 with a phone",
    "filters": { "title": ["VP Sales"], "country": ["US"], "hasPhone": true },
    "limit": 5000,
    "columns": ["person_id", "profile_url", "phone_N_number", "phone_N_type"],
    "caps": { "phones": 2 }
  }'

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 number cost a record?

Yes. A 200 with found: false costs 1 record, because the lookup ran. A 404, where we hold no record for the person, costs nothing. Filter with hasPhone=true to avoid paying for empty answers.

Can I look up a phone number by email or by name?

Not on this endpoint. Use GET /v1/people/phone or the MCP tool for a work email. For a name, search by title and company domain, then send the person_id here. See how to find a VP of Sales phone number.

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, so no key is pasted. The tool is leadocean_find_phone, 1 record. To run a list from Claude, use the Find Phone Number skill.

Get your phone number API key free

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →