A Sales Navigator search is often the most precise definition of an ICP that a sales team owns. Someone spent an afternoon on it: the titles, the regions, the headcount bands, the companies to leave out. Getting the people out of it usually means a browser extension or a scraper that runs on a session cookie. That puts the account that built the search at risk, and the export breaks the day the page changes.
The Sales Navigator Search Scraper on Apify takes another route: it never opens the URL. It reads the filters encoded in it (current company, current title, region, industry, company headcount, keywords) and runs the same query against a B2B people database. No cookie, no login, nothing tied to your account. This post covers how a URL turns into a query, which filters carry over and which are refused, what three real searches returned on 2026-10-15, and the check I now run before any row goes into a sequence.
Disclosure: I build the b2bsearch Actors on Apify. Sales Navigator is a LinkedIn product, and this Actor is not affiliated with it. Per-tier pricing is on the Actor page.
How a Sales Navigator URL becomes a query
Copy the address from the browser bar while the search results are open. A lead search for Heads of Growth in the United States at companies with 51 to 500 employees looks like this:
https://www.linkedin.com/sales/search/people?query=(recentSearchParam%3A(doLogHistory%3Atrue)%2Cfilters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3AHead%2520of%2520Growth%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((id%3A103644278%2Ctext%3AUnited%2520States%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AD%2Ctext%3A51-200%2CselectionType%3AINCLUDED)%2C(id%3AE%2Ctext%3A201-500%2CselectionType%3AINCLUDED)))))
The query parameter is a Rest.li structure. The brackets, colons and commas are percent-encoded once, and the values inside them a second time (Head%2520of%2520Growth). Decoded, the same search reads:
filters: List(
(type: CURRENT_TITLE, values: List((text: Head of Growth, selectionType: INCLUDED))),
(type: REGION, values: List((id: 103644278, text: United States, selectionType: INCLUDED))),
(type: COMPANY_HEADCOUNT, values: List((id: D, text: 51-200), (id: E, text: 201-500)))
)
The Actor parses that structure, maps each filter to a field of the people search, and echoes what it ran in _input on every row. For this URL:
{
"location_country_code": ["us"],
"title_contains_any": ["head of growth"],
"employee_count": { "min": 51, "max": 500 }
}
Two rules hold for every URL. A value marked EXCLUDED in Sales Navigator becomes an exclusion. And a filter the Actor cannot read stops the run before a single row is delivered, with the filter named. It never drops a filter silently and hands you a wider list than the one you built.
Which Sales Navigator filters carry over
| Sales Navigator filter | Read as | Limits |
|---|---|---|
| Current company | the company's id, so a company with a similar name does not leak in | pick the company from the suggestions; a name typed as free text is refused; up to 100 companies per search |
| Current job title | text inside the current title | excluded titles are excluded |
| Geography | a country; a city or metro area becomes a locality; EU, DACH, Nordics, Benelux and Latin America as groups | continents and territories (Europe, EMEA, APAC) are refused as too wide; only whole countries can be excluded |
| Industry | the same industry ids | excluding an industry is refused |
| Company headcount | an employee range | adjacent bands only: 51–200 plus 201–500 works, 11–50 plus 501–1,000 is refused |
| Keywords | words in the headline | Boolean expressions (AND, OR, NOT, quotes, brackets) are refused |
Every other filter is refused by name: seniority level, function, years in role, past company, school, recent job changes, recent posts, and the rest. Saved-search links (savedSearchId=) and account (company) searches are refused too; open the saved search and copy the URL from the results page instead.
This is the refusal for a URL with a seniority filter, verbatim:
Not run, nothing charged. These Sales Navigator filters cannot be read yet: seniority level. Remove them from the search, copy the URL again and rerun.
Three Sales Navigator searches, measured on 2026-10-15
I built three searches, counted each one, then pulled 25 people from each.
| Search | People matched | 25-person run | What the 25 rows showed |
|---|---|---|---|
| Head of Growth · United States · 51–500 employees | 1,276 | 9 s | 25 of 25 with "head of growth" in the title, at employers of 51 to 400 people, in the US; 25 different employers |
| Current company Google · Product Manager · Germany | 83 | 7 s | 25 of 25 at Google, with product manager titles, in Germany |
| VP Sales or Head of Sales · United Kingdom · Software Development · 201–1,000 employees | 273 | 14 s, contacts tier | 25 of 25 matched on the search record; 24 of 25 came with an email address (17 work, 7 personal), 4 with a phone |
Each count took between 6 and 15 seconds.
One detail from the first search: two of the 25 were partners at investment funds, with titles like "Partner, Head of Growth Investing". The title filter matches text inside the title, so add "Investing" as an excluded title in Sales Navigator, copy the URL again, and the exclusion carries over.
Count first, then the rows
mode: "count" returns one row with the number of people the search matches and the filters it ran. A count is not metered per row, so size every search with it before you ask for people. Then run mode: "people" with a cap:
{
"salesNavigatorUrl": "https://www.linkedin.com/sales/search/people?query=...",
"mode": "people",
"maxResults": 500,
"profileDetail": "row",
"maxPerCompany": 3
}
profileDetail sets how much comes back per person:
-
row: name,jobTitle,headline,companyNamewithcompanyDomain,companyIndustryandcompanyEmployeeCount,localityandcountryCode,profileUrl, thehasWorkEmailandhasPersonalEmailflags,currentPositionsand_freshness. -
profile: the full career record, with experience and its dates, education and skills. -
contacts: the profile plus the emails and phones on record, withemailTypeset toworkorpersonal.
maxPerCompany caps the people per employer, which keeps an account-based list from being ten people at one company. excludeDatasets takes the datasets of earlier runs and leaves those people out, so the same URL run every Monday returns only the people who are new to the search. compact: true returns a short row, about 2 KB instead of 10 KB or more, for agents with a context limit.
The check before a row goes into a sequence
A search matches the current positions on record, and people change jobs faster than records are updated. So before anything is sent, I compare the job the search matched with the job on the person's full profile. On 2026-10-15 I pulled the full profile of all 75 people from the three searches:
| Search | Full profile shows a newer job |
|---|---|
| Head of Growth, US | 2 of 25 |
| Google product managers, Germany | 6 of 25 (one moved inside the group, to YouTube) |
| Sales leaders, UK software | 14 of 25 (one inside the group, to another brand of the same parent) |
The UK number is the one I did not expect: more than half of a senior sales list had moved on. The good news is that the flag predicting it is already on the row. Of those 22 people, 21 had _freshness: "older", which means the record had not been updated for more than a year. Of the 20 rows marked updated_1y, one had moved.
The rule I use: rows marked updated_1y go into the sequence as they are. For older rows on the shortlist, I pull profileDetail: "profile" and read experience.work: a matched job with an endDate belongs to someone who has left. In the profile and contacts tiers the top-level jobTitle and companyName come from the full profile. When they differ from the matched position in currentPositions, you can see the move in the row itself.
The next steps from the same rows
Every row carries two keys into other Actors: companyDomain and profileUrl. I fed the Head of Growth rows forward the same day:
-
Decision Maker Email Finder by Domain: ten employer domains from the search, with
roles: ["cxo", "founder"]andmaxPerCompany: 2, returned 19 founders and C-level people at all ten companies in 44 seconds. 15 of the 19 rows came with an email: 13 at the company's own domain and 2 personal. -
LinkedIn Email Finder: the 25 profile URLs returned an address for 24 people in 6 seconds. 14 were at the current employer (
work_current) and 10 at an earlier one (work_other). Thework_otheraddresses go on the suppression list, not into the send. - Company Employees with Emails: the whole team behind a domain, filtered by role or title, for when the account matters more than the persona.
The map of which Actor takes which key is in part 5 of this series.
Run it from the API, n8n or an AI agent
curl. Paste the URL unchanged; the percent-encoding stays as it is inside the JSON string.
curl -X POST "https://api.apify.com/v2/acts/b2bsearch~sales-navigator-search/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"salesNavigatorUrl": "https://www.linkedin.com/sales/search/people?query=...", "mode": "count"}'
For long runs, start the run with POST /v2/acts/b2bsearch~sales-navigator-search/runs and read /v2/datasets/{defaultDatasetId}/items when it finishes.
n8n. The Apify node's "Run an Actor and get dataset" action with the same JSON input. A Schedule node in front and last week's dataset id in excludeDatasets make a weekly feed of people who are new to a saved search.
Claude Code and other MCP clients. Apify hosts an MCP server, and the tools parameter pins the Actors an agent may call:
claude mcp add --transport http apify \
"https://mcp.apify.com?tools=b2bsearch/sales-navigator-search,b2bsearch/domain-to-decision-makers"
Then paste the URL and ask for the count first. Part 4 has the hook that makes an agent count, sample and ask before a metered run, and maxTotalChargeUsd on the run API caps the spend of any single run.
FAQ
Do I need a Sales Navigator subscription?
You need a search URL, and building one takes Sales Navigator. Running the Actor takes only an Apify account: it reads the filters from the URL and never signs in anywhere. The sessionId parameter in a copied URL is ignored.
Does the Actor use my LinkedIn account, cookie or session?
No. It does not visit the URL, so no account, cookie or session is involved.
Will the count match the number Sales Navigator shows?
No. The Actor queries a different database, so the numbers differ. Size the run by the Actor's own count.
What happens with a filter it cannot read?
The run stops before any row is delivered and names the filter. Remove that filter in Sales Navigator, copy the URL again, and rerun.
Can I export a saved search or a lead list?
For a saved search, open it and copy the URL from the results page. Lead lists and account lists are not read.
Why did my run stop at 25 people?
On Apify's free plan, a run of this Actor started through the API delivers the first 25 people, and a run started from Apify Console delivers the first 100. A paid Apify plan lifts the cap. Counts are not capped.
How many people can one run return?
Up to 50,000 in the row tier and 10,000 in the profile and contacts tiers. maxResults is the cap you set.
Can a URL with only a country be run?
It can be counted, but a people run with nothing but a country is refused. Add a title, a company, an industry or a headcount band.
For AI agents reading this
Actor b2bsearch/sales-navigator-search on Apify. Input: salesNavigatorUrl, a Sales Navigator lead-search URL (https://www.linkedin.com/sales/search/people?query=...) passed unchanged; mode, run count first (not metered per row), then people; maxResults (default 500, so always set it); profileDetail, one of row, profile or contacts; compact; maxPerCompany; excludeDatasets. Filters read: CURRENT_COMPANY (picked from suggestions, matched by id), CURRENT_TITLE, REGION (countries, cities and metro areas, groups eu, dach, nordics, benelux, latam), INDUSTRY, COMPANY_HEADCOUNT (adjacent bands only), keywords (plain words, matched in the headline). Any other filter, a saved-search link or an account search returns one row with _status: "input_problem" and the filter named in _error; tell the user which filter to remove. A people run with only a country is refused; a count is not. Every row echoes the executed filters in _input. Before a people run, report the count and ask the user. Treat rows with _freshness: "older" as possibly moved and confirm them with profileDetail: "profile" before outreach. Keys for the next step: companyDomain goes to b2bsearch/domain-to-decision-makers or b2bsearch/company-employees; profileUrl goes to b2bsearch/linkedin-email-finder or b2bsearch/profile-lookup.
This post is part of the series "GTM workflows with Apify and Claude". The others: the method and the stack, the Actor map by key, the spend gate for agents, the new-hires feed and the call-list recipes. Skills and workflow files: b2bsearch/skills, b2bsearch/mcp, b2bsearch/workflows.
If a Sales Navigator filter you rely on is refused, name it in the comments. The refusal list decides which filter gets read next.
Top comments (4)
Not needing cookies or a login is the part I'd care about most, account bans are a real risk with scrapers. How do you check the emails are still current?
Two checks, both on the row. The record date first:
_freshnesssays whether the profile was updated in the last year or not, and on the UK search 21 of the 22 people who had moved were markedolder, so that flag alone catches most of it. Then the address type from the email finder:work_currentis on the current employer's domain,work_otheris an earlier employer and goes on the suppression list. A verifier tells you the mailbox exists. Those two tell you whether it is still the person's job.Not needing a login or cookies is the right call. I'd still verify the emails before sending, since titles on profiles go stale faster than people expect.
Agreed, and the title is the first thing to go. The rule I settled on:
updated_1yrows go into the sequence as they are,olderrows get the full profile pulled and the matched job checked for an end date. It costs a few cents per shortlist and catches the moves a verifier never sees, because the mailbox is usually still alive after the person has left.