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.
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:
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)
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)
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.
| Parameter | Type | Required | Meaning |
|---|---|---|---|
phone | string | One of the two | A number in any common format. |
email | string (email) | One of the two | An 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.
{
"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.
| Field | Meaning |
|---|---|
data.person_id | The record that answered. Send it back as contact_id to the other v2 endpoints. |
data.person_group_id | The person across duplicate records. Compare it, never look it up. |
data.linkedin_url, full_name, job_title, company_name, company_domain | The identity. Each can be null. |
data.query | The number you sent, as we compared it. |
data.matched | The 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_record | true when the record lists the exact number. false means a match upstream, not "nobody found". |
meta.credits | Records 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.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Validation failed. details names the fields. | Send exactly one of phone or email. |
| 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 | No person holds this number. | Stop. It costs nothing. |
| 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
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.
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 →