Before you start
- A connected LinkedIn account to run on, or the managed path to run on our infrastructure.
- The
can_act_linkedin_searchespermission on your API key, agent or user. - A Sales Navigator seat on the executing account for the Sales Navigator methods.
- A Recruiter seat, with a live Recruiter session, on the executing account for the Recruiter methods.
A search is a discovery list, not an enriched one. Rows carry an identity (member id, vanity slug, name, avatar) and
nothing else: no headline, no title, no company, no location, no connection degree. Feed the ids into
enrichment for those, a separate surface. The one exception is the Recruiter people search, whose
rows come back with the headline, current position, location and connection degree filled, because LinkedIn Recruiter
resolves the profile inline.
result.data_request) and one activity-log entry on the executing
account. Nothing is stored, so nothing can be deduplicated away: rows arrive in LinkedIn’s own order, with no sort axis
and no cross-page deduplication. Past roughly five pages, or for anything on a schedule, use an
auto scrape.
The engines
Every method is aPOST under https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/.
The same surface also searches posts, jobs, events, groups, courses, products and schools, and reads a post’s commenters,
reactors and resharers. Same rules, and their filter vocabularies are in the API reference.
Two ways to address a search
The five list searches take either a pastedurl or a structured filters object. Same route, same parser,
same price: the structured half composes the same LinkedIn URL and runs the same page. Send filters when a program or
an agent composes the search, url when a human built it in LinkedIn or the vocabulary cannot express it.
Which half you sent only changes the label in the ledger: input_kind is url or params, and Data Requests shows
“Search people by URL” or “Search people by params” accordingly.
A pasted URL is prefix-checked first: a URL from another LinkedIn screen would scrape the wrong thing. On
https://www.linkedin.com, the regular engines take /search/results/people/, /search/results/companies/ and
/search/results/services/; the Sales Navigator pair takes /sales/search/people and /sales/search/company, no
trailing slash, checked literally. Your page overrides the page or offset baked into the URL; every other query
parameter rides through untouched.
curl
linkedin_account_sid the service picks one of your connected accounts with remaining budget; swap
filters for a pasted "url", or pin "linkedin_account_sid": "ln_ac_YOUR_ACCOUNT" to run the same page on a
specific account.
Filters
Every member, type and bound, engine by engine, is in the API reference. What cuts across all five engines:Three filters that do not mean what they look like
Types and bounds are in the reference; these are the semantics behind them, and getting one wrong costs a run rather than an error.The Recruiter search
search-recruiter-people runs the LinkedIn Recruiter people search on an account that holds a Recruiter seat, addressed
by filters or by a pasted Recruiter url, never both. The filter object mirrors the Recruiter search panel. Chip facets (job_titles, occupations, skills, companies, current_companies,
locations, postal_codes, industries, schools, fields_of_study, degrees, spoken_languages) take arrays of
{id, text, exclude, required, scope}: id comes from recruiter-param-id-lookup, text is the row’s
display_name, exclude negates the chip, required makes it a must-have, and scope is the facet’s own axis
(where in the career for titles and companies, lives-there or would-move-there for locations, minimum proficiency for
spoken languages). Job titles, occupations, skills, companies and postal codes need the text even when they carry
an id, because LinkedIn refuses an entity chip without its label; job titles, skills and companies also accept a
chip with only text. Postal codes need the zip lookup id. School ids are the school’s organization urn, exactly
as the school lookup returns them. The closed sets (network, seniority_levels,
functions, company_sizes, company_types, profile_languages, recently_joined, workplace_preferences,
employment_types) take code arrays, listed in the API reference; note the Recruiter
seniority codes run 1 to 10 and are not the Sales Navigator ones. The three year sliders take {min, max} in 1 to
30, where 30 means 30 and more.
A page holds 25 rows and the ceiling is page 40. Unlike the other engines, paging.total is LinkedIn’s real count.
The Recruiter URL is a handle, not a query
A Recruiter URL carries no filters. After a search, Recruiter showshttps://www.linkedin.com/talent/search?searchContextId=...&searchHistoryId=21502096450&searchKeyword=&start=0&uiOrigin=FACET_SEARCH:
the facets live on LinkedIn’s side under searchHistoryId, the seat’s “Recent searches” entry. Passing such a URL
replays that stored search on the page you ask for (the URL’s own start is ignored). A URL without a
searchHistoryId is refused with 422, because it names no search; LinkedIn is never asked for a keywords-only
search with your filters silently dropped.
Every answer returns search_history_id and search_url. Run page 1 with filters, then page on with
url: search_url and page: 2, 3, ...: no filter object and no typeahead round trips on the later pages, and the same
handle re-runs the search later. Both halves count the same against the daily scraping budget.
An executing account without a Recruiter seat answers 422 recruiter_required. One whose Recruiter session has expired
answers 409 recruiter_reauth_required until the seat holder signs in to Recruiter again in the account’s browser, the
same rule as the Recruiter inbox.
Resolving the ids that filters need
Three typeahead endpoints turn a human term into an id, over separate id spaces: an id from one never works on the others.recruiter-param-id-lookup feeds the Recruiter search only: one type out of occupation, skill, company,
geo, zip, industry, school, fieldOfStudy, degree, language and group, a required query, a fixed ten
rows in the Sales Navigator row shape. Its ids are urn:li:ts_* urns, except language, whose id is the lowercase
language name the spoken-language facet wants.
A lookup types into the account’s already-open tab, so it loads no page, spends no LinkedIn search and leaves the
commercial-use limit untouched. It still takes one slot of the daily
scraping budget, and is never
cached. Empty rows means the dropdown offered nothing for that term: an answer, not an error, ledger row completed.What comes back
Rows sit atresult.rows, paging at result.paging, and the ledger row at result.data_request.
The Recruiter rows add
talent_id (what send-recruiter addresses as recipient_profile_id), industry,
can_send_inmail, open_to_work, connections_count and anonymized, and the result block adds
search_history_id and search_url, the handle to page by.
Those last five are null by construction on the first two engines: the result page does not carry them. Service-provider rows
use the same shape, with no ratings, review counts or service labels. When you need more, take ln_id or nickname
from a people-search row, or sn_id from a Sales Navigator row, into enrichment (as profile_id / public_identifier)
or into check-degree. ln_member_id is a join key only: it cannot be turned back into a signed URN, so no method
accepts it.
To resolve a Sales Navigator company into a vanity slug, run
company-public-identifier
(POST /api/linkedin-enrichment/company-public-identifier).
search-people carries an extra boolean, commercial_use_limit_hit: true means LinkedIn’s monthly commercial-use
paywall fired and rows may be truncated. The field is absent on the company engines, always false on service providers.
Which account runs a search
There is no silent fallback for a pinned account: out of budget it returns
429 rather than quietly moving to
another account. With no account connected at all, the call is a 422 telling you to connect one first.
Daily budget
Every method on this surface, searches and typeaheads alike, spends one shared per-account bucket calledscraping.
The free plan clamps the bucket to 15 scraping calls per day per account, and a young
account ramps lower still while it warms up. Read the bucket
before a batch rather than discovering it through a 429: smart limits and warmup.
A search is synchronous and bounded at 120 seconds. Past that the run is abandoned and the row goes to
failed. The typeaheads run on a 90 second ceiling.Zero rows is usually not a failure
Every case below returns HTTP 200 with"rows": []. Stop as soon as one explains what you see.
- Check
commercial_use_limit_hit.trueonsearch-peoplemeans LinkedIn truncated the page behind its monthly paywall. Re-run onsearch-sales-nav-people, which has no such limit, or wait for LinkedIn’s monthly reset. - Count what the page could show. The regular people parser keeps only cards carrying a vanity slug, an id and a
real name, and drops any card named “LinkedIn Member”, so out-of-network people that LinkedIn renders anonymously
never reach
rows. The Sales Navigator parser has no such filter and often returns more for the same query. - Check the executor’s session. An account whose LinkedIn session is degraded or sitting behind an auth wall returns a correctly shaped page with zero rows. Confirm the account and its browser are healthy.
- Re-read your filter names. A misspelled member is dropped silently, so the search ran less constrained than you meant.
- Check the page number. A page past the end of the results has nothing to return. A page above 100 is not this
case: it is a
422before anything runs. - Read the ledger row.
status: "completed"with zero rows means LinkedIn genuinely answered with nothing;status: "failed"with anerror_codemeans the response could not be read. Never the same thing.
Errors
The detail sits in one of two places, never both: a 422 carries it inerror.field_errors, everything else in
error.context.
The Sales Navigator refusal spells out its own fix: use an account that has a seat, or run
get-my-sales-navigator to
refresh that account’s premium status first. That check also runs on its own every 360 minutes (6 hours), so a seat
bought minutes ago usually needs the manual refresh.