Enrichment API

Reverse Phone Lookup API

GET /v2/people/reverse. One phone number in, the person who holds it out. 1 record per hit, 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/reverse takes one phone number and returns the person who holds it: name, LinkedIn URL, title and employer. It costs 1 record per hit. A 404, where nobody holds the number, costs nothing.

This page is the developer reference. For what the data is and where it is thin, read Reverse phone lookup. Agents call the same lookup through the leadocean_get_lead MCP tool, set up in Reverse Phone Lookup over MCP. The full list of lookups is 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. The number below is a placeholder.

bash
curl -s -G "https://api.leadocean.io/v2/people/reverse" \
  --data-urlencode "phone=+1 415 555 0133" \
  -H "x-api-key: $LEADOCEAN_API_KEY"

The endpoint also answers POST with a JSON body:

bash
curl -s -X POST "https://api.leadocean.io/v2/people/reverse" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{ "phone": "+1 415 555 0133" }'

Node.js (fetch)

js
const url = new URL("https://api.leadocean.io/v2/people/reverse");
url.searchParams.set("phone", "+1 415 555 0133");

const res = await fetch(url, {
  headers: { "x-api-key": process.env.LEADOCEAN_API_KEY },
});

if (res.status === 404) {
  console.log("Nobody holds this number. 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.full_name, data.job_title, data.company_domain);
  console.log(data.matched_in_record ? "Exact number on record" : "Matched upstream");
}

Python (requests)

python
import os
import requests

r = requests.get(
    "https://api.leadocean.io/v2/people/reverse",
    params={"phone": "+1 415 555 0133"},
    headers={"x-api-key": os.environ["LEADOCEAN_API_KEY"]},
    timeout=10,
)

if r.status_code == 404:
    print("Nobody holds this number. Not billed.")
else:
    r.raise_for_status()
    data = r.json()["data"]
    print(data["full_name"], data["job_title"], data["company_domain"])

Request

Send exactly one of phone or email, never both. The key needs the enrich scope, which Free keys have.

ParameterTypeRequiredMeaning
phonestringOne of the twoA number in any common format.
emailstring (email)One of the twoAn address, for a reverse email lookup. Same endpoint, same price. See Reverse Email Lookup API.

There is no lookup by name. To go from a name to a number, search by company domain and title, then call GET /v2/people/phone with the person_id.

Response

Response shape from the API reference. Every value below is a placeholder.

json
{
  "success": true,
  "data": {
    "person_id": "1234567",
    "person_group_id": "1234567",
    "linkedin_url": "https://www.linkedin.com/in/jane-doe",
    "full_name": "Jane Doe",
    "job_title": "Finance Manager",
    "company_name": "Acme",
    "company_domain": "acme.com",
    "query":   { "phone": "+14155550133" },
    "matched": { "...": "..." },
    "matched_in_record": true
  },
  "meta": { "source": "leadocean", "key_hash": "...", "schema": "leadocean.contact.reverse.v2",
            "stability": "stable", "credits": 1 }
}

matched is the number as the record holds it. Its inner keys follow the API reference and are shown as ... above.

FieldMeaning
data.person_idThe record that answered. Send it back as contact_id to the other v2 endpoints.
data.person_group_idThe person across duplicate records. Compare it, never look it up.
data.linkedin_url, full_name, job_title, company_name, company_domainThe identity. Each can be null.
data.queryThe number you sent, as we compared it.
data.matchedThe number as the record holds it, with its own type, status and check date (see the API reference for the inner keys). Null when the record does not echo it back.
data.matched_in_recordtrue when the record lists the exact number. false means a match upstream, not "nobody found".
meta.creditsRecords counted for this call.

This is the one v2 endpoint that sends meta. It returns the identity only. For the full profile, call POST /v1/people/enrich, which costs the same and reads the same cached record. There is no dnc field, and its absence is not clearance: run your own do-not-call check.

Errors

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

StatusMeaningWhat to do
400Validation failed. details names the fields.Send exactly one of phone or email.
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.
404No person holds this number.Stop. It costs nothing.
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

A call that returns a person is 1 record, and a 200 with sparse fields still costs 1. A 404, 402, 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.

Past a paid account's records, calls are paced to 1 a minute and X-Quota-State: over-limit appears in the headers. Alert on that header.

Bulk

POST /v1/exports is the bulk path, but it is built from filters. The docs describe no way to upload a list of numbers to it. A list of numbers means one call here per number, at up to 100 a second.

If you want people with phones rather than a lookup, export by filter instead. One row is 1 record whatever the columns, up to 50,000 rows per export. Contact columns are numbered: you request phone_N_number and email_N_address, and the CSV comes back as phone_1_number, phone_2_number, email_1_address and so on.

bash
curl -X POST https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "Finance managers with a phone",
    "filters": { "title": ["Finance Manager"], "country": ["US"], "hasPhone": true },
    "limit": 5000,
    "columns": ["person_id", "profile_url", "phone_N_number", "email_N_address"],
    "caps": { "phones": 2, "emails": 1 }
  }'

The reply is a 202 with a job id. Poll GET /v1/exports/{id}, then fetch /v1/exports/{id}/download.

FAQ

Does a number nobody holds cost a record?

No. A 404 costs nothing. A 200 that names a person costs 1 record, even when most fields are null.

Can I look up by name?

No. Search people by company domain and title, then call GET /v2/people/phone or POST /v1/people/enrich with the person_id.

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. Pass phone to leadocean_get_lead, 1 record, and a miss is free.

Get your reverse phone lookup API key free

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

Get your free API key →