Sales Navigator people search
Paste a search URL; we paginate it. 50 credits a request of 100 rows.
https://api.easydata.win/v1/sales/search/peoplePaste a Sales Navigator people-search URL and say how many rows you want. We paginate it 100 rows at a time and deliver one result entry per page, each landing as it is scraped - so a 2,500-row search is readable from the first page rather than after the twenty-fifth.
We do not build the search. There is no facet resolution, no query construction and no interpreting what you meant: the URL you paste is the search, exactly as Sales Navigator wrote it.
The ceiling is 2,500 rows and is enforced at submit time. It is LinkedIn's own limit - the search itself stops serving past it - so finding out before you pay for anything is the only useful moment to be told. A full 2,500 is 25 requests and 1,250 credits.
max_results rounds UP to a whole request. A request costs the same whether we ask it for a hundred rows or for thirty, so asking for 150 fetches two, bills two, and hands you both - about 200 rows. There is nothing to save by asking for less than a request holds, so we give you the rest instead of discarding it.
Set find_emails: true and every row also carries an emails object with a work email address in it, for a flat +4 per row we reach a verdict about. It does not need enrich: a search card already carries the name and the current employer, which is everything a lookup needs. See Finding email addresses.
Need it in the response?POST https://api.easydata.win/v1/sales/search/people/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 #
| Field | Type | Notes |
|---|---|---|
targetsrequired | array | One or more pasted Sales Navigator search URLs, as { "searchUrl": "..." } or a bare string. Each is paginated independently. |
max_resultsrequired | integer | How many rows you want FROM EACH search URL, and the number you are billed for as they arrive. Every entry in targets is paginated independently, so two search URLs with max_results 500 is up to 1,000 rows and a quote of 500 credits - not 500 rows shared between them. Above the ceiling this is refused at submit time - that ceiling is LinkedIn's own, not ours, and it applies per search URL for the same reason. |
enrich | boolean | Fold 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_emails | boolean | Fold 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_id | string | Your 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_url | string | Where 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_tag | string | Route 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 -X POST "https://api.easydata.win/v1/sales/search/people" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"targets":[{"searchUrl":"https://www.linkedin.com/sales/search/people?query=(filters:List())"}],"max_results":500}'Response #
{
"data": {
"batch_id": "afec9131-080b-4b96-b3bb-726249e4bbf8",
"operation": "sales.search.people",
"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.
{
"item_index": 0,
"page": 1,
"input": { "searchUrl": "https://www.linkedin.com/sales/search/people?query=(filters:List())" },
"status": "succeeded",
"credits_used": 50,
"data": {
"results": [
{
"urn": "ACwAAAyHg0MB",
"member_id": 219386421,
"url": "https://www.linkedin.com/in/ACwAAAyHg0MB",
"first_name": "Elina",
"last_name": "Virtanen",
"full_name": "Elina Virtanen",
"headline": "Head of Engineering",
"location": "Helsinki, Finland",
"premium": true,
"open_link": false,
"memorialized": false,
"opted_out_of_data_sharing": false,
"current_positions": [
{
"title": "Head of Engineering",
"current": true,
"company_name": "Supercell",
"company_id": 1042569,
"company_url": "https://www.linkedin.com/company/1042569",
"tenure_months": 41
}
],
"scraped_at": "2026-09-01T10:00:12Z"
}
],
"pagination": {
"page": 1,
"returned": 100,
"has_more": true,
"total": 4182,
"display_total": "4,182"
},
"search_title": "Engineering leaders, Finland"
},
"created_at": "2026-09-01T10:00:12Z"
}The 50 credits on the entry above are one request, which is what a page of 100 rows is. The page is the unit because the request is what we spend upstream, so a page that comes back short or empty costs the same 50 as a full one - and a search that ends early is charged for the requests it made, not for the rows they happened to hold.
pagination.total is advisory. On a broad search LinkedIn reports the whole universe - tens of millions - while the search itself stops serving at 2,500. Use has_more to decide when you are done, never total.
Errors #
| Status | Type | When |
|---|---|---|
| 400 | invalid_request | The 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. |
| 401 | invalid_api_key | Missing, malformed, revoked or expired key. Never metered. |
| 403 | quota_exhausted | This month's allowance cannot cover the submission. Nothing is reserved and nothing is charged. |
| 403 | email_unverified | Nobody 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. |
| 429 | rate_limited | Too 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. |
| 422 | capacity_unavailable | No Sales Navigator capacity was free. You get this rather than a silently thinner result, and the item is not charged. |
| 400 | invalid_request | max_results above 2,500, or a URL that is not a Sales Navigator search. |
Every failure uses the same envelope, and none of them is metered - see Errors.