Enrichment API

Company Employees API

GET /v1/companies/{domain}/people. One domain in, the people we hold there out, up to 100 a page. 1 record per person returned, a 404 is free.

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

GET /v1/companies/{domain}/people takes a company domain and returns the people we hold there, up to 100 a page. It costs 1 record per person returned. A 404 costs nothing.

This page is the developer reference. For what the data is and how to read it, see list the employees of a company. Over MCP, the same roster is leadocean_search_leads with the domain filter. Related: the company enrichment API guide and the best company data APIs.

Quick start (curl)

Create a free account, make a key and export it as LEADOCEAN_API_KEY. The key needs the search scope (this endpoint is filed under Search in the API reference).

bash
curl -s "https://api.leadocean.io/v1/companies/acme.com/people?limit=100" \
  -H "x-api-key: $LEADOCEAN_API_KEY"

The domain is a path parameter. There is no lookup by company name. Each page carries meta.nextCursor. Pass it back unchanged to get the next page.

bash
curl -s "https://api.leadocean.io/v1/companies/acme.com/people?limit=100&cursor=$CURSOR" \
  -H "x-api-key: $LEADOCEAN_API_KEY"

Node.js (fetch)

js
const base = "https://api.leadocean.io/v1/companies/acme.com/people";
const people = [];
let cursor = null;

do {
  const url = new URL(base);
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

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

  if (res.status === 404) break; // nobody held for this domain. Not billed.
  if (res.status === 429) {
    const wait = Number(res.headers.get("retry-after") ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
    continue;
  }
  if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error?.message}`);

  const body = await res.json();
  people.push(...body.data);
  cursor = body.meta?.nextCursor ?? null;
} while (cursor);

console.log(`${people.length} people`);

Python (requests)

python
import os
import requests

url = "https://api.leadocean.io/v1/companies/acme.com/people"
headers = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
people, cursor = [], None

while True:
    params = {"limit": 100}
    if cursor:
        params["cursor"] = cursor
    r = requests.get(url, params=params, headers=headers, timeout=10)
    if r.status_code == 404:
        break  # nobody held for this domain. Not billed.
    r.raise_for_status()
    body = r.json()
    people.extend(body["data"])
    cursor = body.get("meta", {}).get("nextCursor")
    if not cursor:
        break

print(len(people), "people")

Request

ParameterInTypeRequiredMeaning
domainpathstringYesThe company domain, for example acme.com.
limitqueryintegerNoPeople per page, 1 to 100. Default 25.
cursorquerystringNoThe meta.nextCursor of the previous page, passed back exactly as returned.

There is no filter on this endpoint. To narrow by title, seniority or country, run a people search with the domain filter instead. It takes the same cursor and the same page size.

Response

Response shape from the API reference. Every value below is a placeholder, not a captured response.

json
{
  "success": true,
  "data": [
    {
      "profile_data": {
        "profile_full_name": "Alex Example",
        "profile_headline": "Head of Sales at Acme",
        "profile_url": "https://www.linkedin.com/in/alex-example"
      },
      "contact_data": {
        "has_email": true,
        "has_phone": false,
        "email_status": "verified",
        "email_type": "work",
        "contact_current_experiences": [
          { "...": "company name, domain, job title, headcount" }
        ]
      },
      "meta": {
        "source_ids": { "person_id": "1234567890" }
      }
    }
  ],
  "meta": {
    "source": "leadocean",
    "count": 100,
    "credits": 100,
    "companyIds": [101],
    "nextCursor": "(opaque string)"
  }
}
FieldMeaning
data[]Thin person records (leadocean.person.v1). Values we do not hold are null.
data[].contact_data.has_email, has_phoneWhether we hold an address or a number. The values themselves are not in this response.
data[].contact_data.email_statusDeliverability of the best address. Thirteen values, from verified to none. List them with GET /v1/enums/email_status.
data[].contact_data.email_typework, work_other (an address at a former employer) or personal.
data[].meta.source_ids.person_idThe record id. Send it to the enrich or contact endpoints for that person.
meta.creditsRecords counted for this page.
meta.companyIdsThe company ids represented in this page. This endpoint only.
meta.nextCursorToken for the next page. Absent or null on the last page.
meta.depthCappedtrue on the page where the walk hit its cap. No cursor follows.

The entries inside contact_current_experiences are objects the spec does not type, so read a real response for their keys. Roughly a fifth of the people search can find have no LinkedIn URL, so profile_url can be null. Use person_id.

Errors

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

StatusMeaningWhat to do
400Validation failed, or the cursor was edited, truncated or issued before 2026-09-19 (Invalid cursor).Read details. For a bad cursor, restart the walk from page one.
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 scope for this endpoint, or the account is suspended.Create a key with the right scope.
404No people held for this domain.Stop. It costs nothing. Check the domain spelling.
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

Every person returned is 1 record. A page of 100 is 100 records. A page cut short at the end of a roster costs only what it returns. Walking 250 people is three pages (100, 100, 50) and 250 records.

A 404 and any rejected request (429, 402, 503) cost nothing. The limit is 100 requests per second per key, on every plan, so ask for full pages rather than making more calls.

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.

Walks stop at 10,000 rows. The last page carries meta.depthCapped: true and no cursor. Narrow with people search and run several smaller searches. Support can raise the figure for a genuine large export.

Size a roster before you pull it. GET /v1/people/search?domain=acme.com&count=true&limit=1 returns a free total in meta.total, capped at 100,000. The domain filter takes several domains at once.

Bulk

For many companies, use POST /v1/exports instead of looping this endpoint. The domain filter takes an array, one export can cover thousands of accounts, and a row is 1 record whatever the columns. The docs call bulk the cheaper way to enrich. A file takes up to 50,000 rows per request. It needs the search scope, plus enrich for email columns.

Email columns are numbered: email_N_address and email_N_status come back as email_1_address, email_1_status, email_2_address and so on. caps sets how many (default 3).

bash
curl -X POST https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "Rosters for target accounts",
    "filters": { "domain": ["acme.com", "example.org"], "emailStatus": ["verified"] },
    "limit": 5000,
    "columns": ["person_id", "profile_full_name", "current_job_title",
                "current_company_domain", "email_N_address", "email_N_status"],
    "caps": { "emails": 1 }
  }'

The reply is a 202 with a job id. Poll GET /v1/exports/{id}, then fetch /v1/exports/{id}/download. The Exports page at app.leadocean.io builds the same file with a filter builder, a column picker and the record price shown first.

FAQ

Does this return email addresses?

No. The roster is thin: it says has_email and email_status, never the address. Enrich the people you want by person_id, or export with email_N_address columns. Revealing an email costs no extra record.

Can I filter by title or seniority?

Not on this endpoint. Run a people search with domain plus title, jobLevel or country. Same cursor, same 100-row pages, same 1 record per person.

Can an agent list employees 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. Use leadocean_search_leads with the domain filter, 1 record per person returned. Count first with leadocean_count_leads, which is free.

Get your company employees API key free

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

Get your free API key →