I run a visa agency in Bali, and the least glamorous job in it is turning a name into a person. A conference badge list, a referral from a client, a staff CV, a row in the CRM that says only "Agus Pratama, Jakarta" — before anyone writes an email, somebody has to find out which Agus that is and where he works now. I automated the step with an Apify Actor that reads LinkedIn's public people directory — linkedin.com/pub/dir, the page LinkedIn serves to logged-out visitors and to search engines when a person is looked up by name. No account, no cookies, no session to get restricted.
To make sure it kept working I pointed a cron job at it months ago with the dullest input I could invent:
{
"names": ["Satya Nadella", "John Smith"],
"maxResultsPerName": 20,
"includeFullProfile": false
}
One famous rare name, one maximally common one. Thirty-one rows, under thirty seconds, about a cent a run. I only ever looked at the exit status. Last night I opened the datasets instead and found I had accidentally built a panel: 27 runs of the same two names between 17 and 31 October 2026, plus a few extra from debugging. Diffing them answered a question I had been guessing at for a year — how real is the number LinkedIn prints at the top of that page?
The headline count fell 13% in eleven days
Every row carries totalMatches, lifted straight from the page's own header: how many people LinkedIn says have the name. For "John Smith" it was not stable.
17–24 Oct (14 runs) totalMatches 111000
26–28 Oct (4 runs) totalMatches 106000
28 Oct 17:06 – 31 Oct totalMatches 96500
Fourteen thousand five hundred John Smiths, gone in eleven days — and the drop landed between two runs eight hours apart on 28 October. LinkedIn did not lose 13% of its John Smiths that afternoon. The number is a rounded, recomputed estimate (notice the granularity itself changing, from thousands to hundreds), and it belongs in your data as order of magnitude, not as a count. I keep it because it answers "is this name rare or common", which is a genuinely useful question when you are deciding whether a name is worth chasing at all. I would not put it in a report.
For "Satya Nadella" the same field read 11 in all 27 runs. Small numbers are exact; big ones are decoration.
The rows underneath it barely moved
The interesting part is what the churn in the header did not do to the cards. Across 27 runs of 20 rows each — 540 rows — the directory showed me 26 distinct people:
appeared in all 27 runs 17 profiles
appeared in 22 runs 1
appeared in 20 runs 1
appeared in 9 runs 3
appeared in 5 runs 1
appeared in 4 runs 1
appeared in 2 runs 1
appeared in 1 run 1
First run versus last run, fifteen days apart: 17 of 20 the same. For a name LinkedIn claims around a hundred thousand people share, the public directory is not a sample of them. It is a fixed shortlist that re-reading does not refresh.
That is the opposite of the lesson most scraped sources teach, and it has a practical edge. If you got 20 John Smiths today and none of them is your John Smith, running the name again tomorrow is not a strategy — you will spend money to receive the same 17 people. Narrow the name instead (a middle name, the last name first), or go at it from the company side with a different tool.
What does move is the order. Membership held at 19 or 20 out of 20 between consecutive runs, while position churned hard:
24 Oct -> 26 Oct 20/20 positions changed
28 Oct 00:25 -> 17:06 20/20 positions changed
30 Oct -> 31 Oct 12/20 positions changed
Twice in fifteen days the entire list reshuffled without changing who was in it. So rank is a record of what the page showed this time, not an identity and not a relevance score you can trend. Join on publicIdentifier; if you store yesterday's rank and diff it, you will invent movement that nobody made.
A rare name is a census, not a window
"Satya Nadella" behaved like a different product. Eleven rows, every run, in the same order for nineteen days straight — the 12 October run and the 31 October run are byte-identical in membership and sequence. When a name is below the ceiling, the directory is complete: you are getting everybody, not the first page of them.
Here is row one of the latest run, untouched:
{
"type": "person",
"searchName": "Satya Nadella",
"rank": 1,
"fullName": "Satya Nadella",
"location": "Redmond, WA",
"currentCompany": "Microsoft",
"moreCompaniesCount": 4,
"schoolsCount": 3,
"school": "Manipal Institute of Technology, Manipal",
"linkedinUrl": "https://www.linkedin.com/in/satyanadella",
"publicIdentifier": "satyanadella",
"totalMatches": 11,
"headline": null,
"scrapedAt": "2026-10-31T07:01:39.918Z"
}
But "complete" is doing some work there, because only 8 of those 11 are named Satya Nadella. The others are Guru Satya Nadella, Satya Datt Nadella, and — row eleven — Dr. Satya Prathyusha, who is not a Nadella at all. LinkedIn's directory matches name parts, not the whole string, and it counts the near-misses in totalMatches. If you need exact identity, compare fullName to what you asked for yourself; the eleven rows are candidates, not confirmations.
The filters do not reach past the window
The Actor takes a locationContains and a companyContains, and both are applied after fetching, on our side, because the public directory has no server-side filters at all. That sounds like a footnote until you measure it on the real 20 rows:
locationContains "London" keeps 1 / 20
locationContains "New York" keeps 3 / 20
locationContains "India" keeps 0 / 20
locationContains "United States" keeps 0 / 20
companyContains "Microsoft" keeps 0 / 20
Thirteen of those 20 rows are plainly in the US — "Spring, TX", "Bethesda, MD", "Honolulu, HI" — and United States keeps none of them, because the card prints what the member typed and nobody types the country. A filter narrows the 20 (or 50) that LinkedIn already chose; it cannot go and find the John Smiths in India. Zero results from a filter means "none of these cards say that word", never "there are none".
The upside of filtering client-side is billing: cards removed by a filter are not charged, and the cap is applied the same way. In my run the directory handed over 61 cards and maxResultsPerName: 20 delivered 31 — the other 30 were discarded without appearing on the bill.
What a card actually carries
Measured on the latest 20 John Smith rows:
location 20/20
currentCompany 19/20
moreCompaniesCount 19/20
schoolsCount 17/20
school 0/20
photoUrl 9/20
headline 0/20
Two things to plan around. schoolsCount usually arrives as a bare count ("2 total") with no school name, so school being null is the normal case rather than a gap. And there is no headline or job title on a directory card, ever — the first listed company is all you get. Titles need the profile page itself, which is the paid add-on in this Actor and also what our LinkedIn Profile Scraper does as its day job.
One row in that batch is called "John S." — the logged-out truncation LinkedIn applies to some members. Its slug is john-smith-ciso, so the name is recoverable, but if you match on fullName alone you will drop him.
Getting at it
curl -X POST "https://api.apify.com/v2/acts/lergassy~linkedin-people-search-by-name/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "content-type: application/json" \
-d '{"names":["Satya Nadella","John Smith"],"maxResultsPerName":20}'
The run above delivered 31 rows and reported $0.01037 of events — $0.001 per name that returns anything, $0.0003 per row. Names with no match, bad input and refused lookups are free, which matters more than it sounds: at scale a chunk of any name list is garbage, and you do not want to pay per attempt. The Actor is LinkedIn People Search by Name if you want the input schema.
Two transport notes, both learned the hard way. The directory answers datacenter and residential addresses with HTTP 999; it opens through a proper unblocking proxy. And when it refuses with 451, retrying identically is pointless — on 30 October a buyer's run lost 15 of 103 names to 451 because all three attempts went out as the same Chrome impersonation, the exact fingerprint being refused. Retries now vary the client instead, which is a cheap fix for a failure mode that looks like rate limiting and is not.
When this is the wrong tool
If you want everyone with a job title at a company, this is not it — it needs a name to start from. If you need more than 50 people for one common name, nothing reading the logged-out directory will give it to you; tools that go deeper sign in with an account and pay for it in ban risk. And if you need email addresses, the public directory has none.
What it is good for is the boring middle of a workflow: a column of names, and a column of LinkedIn URLs and current employers beside it, with the ones that are too common flagged by totalMatches so a human looks at those and only those. Fifteen days of identical runs told me the data is stable enough to cache and shallow enough to respect.
Top comments (1)
The repeated-result pattern is a strong reminder that an API result count is not the same as accessible inventory. I would log the query fingerprint, cursor behavior, and unique-ID cardinality together; that turns a frustrating scrape into evidence of a product constraint.