Sales Navigator search across a company list

One people search, run once per chunk of your company list.

POSThttps://api.easydata.win/v1/sales/search/employees
50  per requesta request brings 100 stubs, and every chunk is at least one request

The people search, filtered to a list of companies you supply. Send the search URL and up to 1,000 companies; we run the search once per chunk of that list and deliver one result entry per page, each saying which chunk it came from.

Why chunks: LinkedIn takes a current-company filter inside the search's own query, and a query has a length. About fifty companies fit beside the filters you already have, so a thousand companies become twenty searches - your filters unchanged, a different fifty on each. The chunking is computed rather than fixed at fifty, because a long search leaves room for fewer, and the entry tells you what it actually used.

Paste the search WITHOUT a company filter in it. One already there is refused rather than merged: your chips and your list are the same filter, and guessing which you meant produces a search quietly narrower or wider than either.

A company is its numeric LinkedIn id. /sales/search/companies and /companies/enrich both hand you one, which is the pairing this operation is built for: an account search to find the companies, then this to find the people inside them.

max_results is the total across every chunk, and its ceiling is a multiple of 2,500 rather than 2,500 - each chunk is its own search, and LinkedIn starts counting again at each one. This is the only way past the 2,500 rows a single search will ever serve.

Need it in the response?POST https://api.easydata.win/v1/sales/search/employees/sync runs the same operation inside your request and hands back the record, for 100per request - twice the price above, because it outranks every queued batch for a worker and holds one open while you wait. It takes target, one entity, never a targets array. See Synchronous requests.

Request #

FieldTypeNotes
targetsrequiredarrayOne or more { "searchUrl": "...", "companies": [...] } objects. The URL is a pasted Sales Navigator PEOPLE search without a company filter in it; companies is the list to run it across. Each target is chunked and paginated independently.
targets[].companiesrequiredarrayThe companies to filter by, up to 1,000, in the order they will be searched. An entry is a numeric LinkedIn company id - bare, as a urn, or as a /company/<id> URL - a company NAME, or an object carrying either: { "id": 1337, "name": "LinkedIn" } or { "name": "Acme Corp" }. An id filters exactly and is what to send when you have one. A name filters too, by the same fuzzy text match a person gets typing into the Sales Navigator box, which is what makes a list of names usable without resolving each one first - the trade is that a name can pull in a company you did not mean. Nothing guesses an id from a name; each chunk.companies entry says which kind of chip it was.
max_resultsrequiredintegerThe total rows you want from this target, ACROSS every chunk of its company list, and the number you are billed for as they arrive. The ceiling is a multiple of LinkedIn's own 2,500 rather than 2,500 itself: each chunk is a separate search that starts counting again, which is the only way past 2,500 rows this API has.
enrichbooleanFold the full record into every row, under profile for people and company for companies. Adds exactly what that enrich endpoint costs, +1 per ROW either way, charged only on the rows that actually resolved. A row whose record could not be fetched arrives as a plain stub and adds nothing to the request its page already cost, and the absent field is how you tell.
find_emailsbooleanFold a work email address into every row, under emails. Adds a flat +4 per ROW that reached a verdict, so a page of a hundred where sixty resolve is +240 rather than +400. Independent of enrich - a search card already carries the name and the current employer, which is everything a lookup needs - so you can ask for addresses without buying the full records. A page with this set arrives whole rather than row by row, the same way an enriched page does.
external_idstringYour own correlation handle. It comes back on the batch, on the completion webhook and in the usage rollup, so spend can be attributed to your pipeline rather than only to ours.
callback_urlstringWhere to deliver batch.completed. Optional - polling is a first-class path, not a fallback. Validated when you send it and again when we dial it. Mutually exclusive with webhook_tag.
webhook_tagstringRoute the completion to the endpoints carrying this tag instead of to the untagged ones, so you can name a destination without knowing its URL. A tag matching no enabled endpoint is refused here rather than discovered as a webhook that never fired.

Call it #

cURL
curl -X POST "https://api.easydata.win/v1/sales/search/employees" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"targets":[{"searchUrl":"https://www.linkedin.com/sales/search/people?query=(filters:List())","companies":[{"id":1337,"name":"LinkedIn"},{"id":1441,"name":"Google"}]}],"max_results":5000}'

Response #

JSON
{
  "data": {
    "batch_id": "afec9131-080b-4b96-b3bb-726249e4bbf8",
    "operation": "sales.search.employees",
    "status": "queued",
    "total": 2,
    "succeeded": 0,
    "failed": 0,
    "pending": 2,
    "results_available": 0,
    "credits_used": 0,
    "external_id": "crm-sync-2026-09-01",
    "created_at": "2026-09-01T10:00:00Z",
    "completed_at": null,
    "recommended_poll_ms": 2000
  },
  "meta": { "requestId": "req_8f2ka91x", "recommendedPollMs": 2000 }
}

A result entry #

Once the batch has drained, this is what reading its results gives you.

JSON
{
  "item_index": 0,
  "page": 1,
  "status": "succeeded",
  "credits_used": 50,
  "data": {
    "results": [
      {
        "urn": "ACwAAAyHg0MB",
        "full_name": "Elina Virtanen",
        "headline": "Head of Engineering",
        "current_positions": [
          {
            "title": "Head of Engineering",
            "current": true,
            "company_name": "LinkedIn",
            "company_id": 1337
          }
        ],
        "scraped_at": "2026-09-01T10:00:12Z"
      }
    ],
    "pagination": {
      "page": 1,
      "returned": 100,
      "has_more": true
    },
    "chunk": {
      "index": 1,
      "chunks": 20,
      "companies": [{ "id": "1337", "name": "LinkedIn" }, { "id": "1441" }, { "name": "Acme Corp" }]
    }
  },
  "created_at": "2026-09-01T10:00:12Z"
}

A chunk costs at least one credit, because it is at least one request to LinkedIn. Fifty companies that employ nobody your search matches still cost the request they took, and a thousand-company list is twenty of those. The floor bites at nought rows and at one; from two rows up the per-row price already covers the request.

An empty chunk still arrives as an entry, with its companies and returned: 0. That is the answer to "does anybody at these fifty match" and there was no way to give it before.

chunk.index against chunk.chunks is also how a run that stopped early reads. A search that is refused mid-walk keeps every page already delivered and charged, and stops: the last entry names the chunk it stopped on, and nothing past it was fetched or billed.

Companies are searched in the order you send them, so chunk 1 is your first fifty. Order the list by how much you care.

Errors #

StatusTypeWhen
400invalid_requestThe body could not be accepted as sent. `error.field` names what was wrong. A field this reference does not list is one of these: an unknown key is refused rather than accepted and ignored, because a typo that is silently dropped is indistinguishable from one that worked.
401invalid_api_keyMissing, malformed, revoked or expired key. Never metered.
403quota_exhaustedThis month's allowance cannot cover the submission. Nothing is reserved and nothing is charged.
403email_unverifiedNobody on the account has confirmed their email address, and the account is on the default allowance rather than an agreed one. Open the link sent when the account was created, or request a new one from the console.
429rate_limitedToo fast: the request budget for your key, too many batches in flight, too many synchronous requests (per minute or at once), or an agreed hourly spend already met. `message` says which. Carries `Retry-After`. Never metered.
422capacity_unavailablePer item: we had no capacity able to run it right now. The message narrows it - Sales Navigator capacity on a search, general capacity elsewhere. credits_used is 0, so retrying later costs you only the wait.
422unprocessable_targetPer item: the search already filters on current company, a company entry carries neither an id nor a name, or the pasted search is so long that no company filter fits beside it. credits_used is 0.
400invalid_requestmax_results above the ceiling, a URL that is not a Sales Navigator people search, or - on /sync only - a company list too long to fit one filter.

Every failure uses the same envelope, and none of them is metered - see Errors.