Documentation

Contact endpoints

Four doors that each ask one question — the work email, the personal addresses, the phone numbers, or who a contact point belongs to — instead of enriching a whole person and sifting the result yourself.

Why these exist

Most of the time you want one fact. Enriching a person returns everything we hold and leaves you to pick — which address is their current work one, which of these numbers is a mobile. These endpoints apply that judgement for you and return the answer alone.

They are doors onto the lookup /v1/people/enrich already makes: same cached record, same meta.key_hash, same one-record price. Mixing v1 and v2 for the same person costs nothing extra and the two cannot disagree with each other.

Price. One record per call, including a call that finds nothing. Asking whether someone has a deliverable work address is already free — a people-search row publishes has_email, email_status and email_type as flags, and sizing a search costs nothing. A free miss here would be a second, metered oracle for a question that is already answered for free.

Identifying the person

The three forward endpoints take exactly one of these. Sending both is a 400 rather than a silent preference; sending neither is a 400 too.

FieldTypeMeaning
contact_idstring (numeric)The id a people-search row carries at meta.source_ids.person_id. The only key that works for the roughly one person in five with no LinkedIn URL. person_id is accepted as the same field under its original name.
linkedin_urlstring (URL)LinkedIn profile URL.

A contact point — an email address or a phone number — is not accepted as a forward key. That is what reverse is for. Every endpoint answers on GET with a query string and on POST with a JSON body.

The current work email

GET /v2/people/email/work · 1 record · scope enrich

One address or null — not a ranked list, because handing back four candidates moves our ranking decision onto your desk. data: null is a 200, not a 404: the person was found and only the address was not, and those are two facts you retry differently.

Only addresses at the employer the record says they work at now qualify, and only those we can stand behind — verified (our own verifier sent to it and it accepted), catch_all_valid (a catch-all domain, but this mailbox confirmed to exist through the provider's identity check) or catch_all (the domain accepts everything, so no per-address test can confirm it either way; labelled rather than silently mixed in). A business address at a former employer is excluded on purpose: it bounces, or worse it does not bounce and reaches whoever inherited the mailbox.

curl -s "https://api.leadocean.io/v2/people/email/work?contact_id=21144152" \
  -H "x-api-key: $LEADOCEAN_API_KEY"
200 OK · application/json
{
  "success": true,
  "data": {
    "email": "ozlem_diker@apple.com",
    "status": "verified",
    "verified_batch_date": "2026-09-14"
  }
}

verified means we checked. Not that a data supplier told us so. verified_batch_date is the date of the verification run that confirmed it — a whole batch shares one date, so it is not a timestamp for that single address. See deliverability statuses.

Personal addresses

GET /v2/people/email/personal · 1 record · scope enrich

A list, because a person legitimately holds several and none outranks the others the way a current work address does. An empty array is the honest answer when we hold none.

No status field, deliberately. We do not verify consumer domains, so nearly every personal address would read the same word and a field that says one thing almost always is noise a reader learns to skip. What we do know is applied rather than printed: addresses we hold a negative verdict on — undeliverable, spam trap, abuse, disposable, risky or a role mailbox — are withheld entirely, because publishing one with no field to notice it on is handing over a bounce dressed as a lead.

curl -s "https://api.leadocean.io/v2/people/email/personal?contact_id=21144152" \
  -H "x-api-key: $LEADOCEAN_API_KEY"
200 OK · application/json
{
  "success": true,
  "data": [
    { "email": "ozlembingol@yahoo.com", "verified_batch_date": null }
  ]
}

Phone numbers

GET /v2/people/phone · 1 record · scope enrich

Every number we hold, best first, each with its type — mobile, direct or office — plus best repeated on its own so a caller who wants one number does not have to index into an array. found: false means we genuinely hold none, not that the lookup failed.

curl -s "https://api.leadocean.io/v2/people/phone?contact_id=21144152" \
  -H "x-api-key: $LEADOCEAN_API_KEY"
200 OK · application/json
{
  "success": true,
  "data": {
    "person_id": "21144152",
    "found": true,
    "phones": [ { "phone": "+15128201206", "type": "mobile" } ],
    "best":   { "phone": "+15128201206", "type": "mobile" }
  }
}

Numbers are shared. Switchboards and reassignment mean one number can belong to several people over time. Treat a match as evidence, not proof, before calling.

Whose contact point is this

GET /v2/people/reverse · 1 record · scope enrich

The opposite direction: give an email address or a phone number, get the person. Takes exactly one of email or phone. Unlike the forward endpoints, the identity is the answer here, so this one returns a thin identity block rather than a single field.

query echoes what you asked; matched is the contact point as the record holds it, with its status and type; matched_in_record tells you whether the exact value you sent appears on the record we returned.

curl -s -G "https://api.leadocean.io/v2/people/reverse" \
  --data-urlencode "email=ozlem_diker@apple.com" \
  -H "x-api-key: $LEADOCEAN_API_KEY"
200 OK · application/json
{
  "success": true,
  "data": {
    "person_id": "21144152",
    "person_group_id": "21144152",
    "linkedin_url": "https://www.linkedin.com/in/…",
    "full_name": "Özlem Diker",
    "job_title": "Finance Manager",
    "company_name": "Apple",
    "company_domain": "apple.com",
    "query":   { "email": "ozlem_diker@apple.com" },
    "matched": { "email": "ozlem_diker@apple.com", "status": "verified",
                 "verified_batch_date": "2026-09-14", "type": "work" },
    "matched_in_record": true
  },
  "meta": { "source": "leadocean", "key_hash": "…", "schema": "leadocean.contact.reverse.v2",
            "stability": "stable", "credits": 1 }
}
person_id names a record; person_group_id names the person. We hold more than one record for some people, and which one a lookup lands on depends on the key you used — so the same human can come back under different person_ids through different contact points. Two records with the same person_group_id are the same person as far as our de-duplication knows. Compare it; never look it up. person_id stays the key you send back.

Stability

v2 is stable. The response shape, the prices and the rules behind the data are all promises now, and every v2 response carries meta.stability: "stable" where it carries meta at all — the three forward endpoints send data and nothing else, because a caller asking one narrow question should not have to step over an envelope to reach the answer.

The generated reference has every parameter and response shape for these four, alongside the rest of the API, and lets you run them in the browser. openapi.json is the same thing for your code generator.