Skip to main content
A search runs now, returns one live page of results inside the HTTP response, and stores nothing.

Before you start

  • A connected LinkedIn account to run on, or the managed path to run on our infrastructure.
  • The can_act_linkedin_searches permission on your API key, agent or user.
  • A Sales Navigator seat on the executing account for the Sales Navigator 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.
A search is never cached: every call re-runs. The only durable traces are one row in the Data Requests ledger (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 a POST 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. The five list searches take either a pasted url 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.
Exactly one of them is mandatory. Sending both returns 422 validation_failed with a field error on each, and sending neither returns the same 422. Sending filters where every member is empty (including an empty `{}`) returns 422 with the message At least one filter member is required. A false counts as empty, so a lone "hiring_on_linkedin": false is a 422 too. An explicit "url": null beside a filled filters is fine, and the reverse.
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
With no 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:
A misspelled filter name is not an error on the people, company and Sales Navigator engines. The unknown key is dropped before dispatch and the search runs unconstrained, so a broad result reads like a filtered one. Copy member names exactly. Retired names are the exception and fail loudly with a 422: title, geo_ids, industry_ids, current_company_ids on Sales Navigator people, service_ids and geo_ids on service providers, headcounts, annual_revenues, headcount_growth_min on Sales Navigator companies.

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.

Resolving the ids that filters need

Two typeahead endpoints turn a human term into an id, over separate id spaces: an id from one never works on the other.
Lookup type school returns real school ids, and the people search has no school-id member: its school is free text. Those ids only reach LinkedIn inside a pasted people-search URL, so do not put them in filters.school, where they are matched as text and return nothing useful. job_location is likewise its own id family, for the job search only.
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 at result.rows, paging at result.paging, and the ledger row at result.data_request. Those last five are null by construction: the result page does not carry them on either engine. 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).
Paging: page is the page you asked for (default 1), page_size is how many rows LinkedIn returned rather than a knob you set, and total is always null on the people and company engines. Drive the loop from has_more, true while this page returned rows and page is below 100. The ceiling is page 100: a page above it is a 422, has_more is false at 100 whatever LinkedIn still has, and an empty page is terminal.
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. 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.
Idempotency replay matches on the team and the key alone, not the method and not the input, so reusing one key after switching a call from filters to url, or after changing the page, hands back the first stored result instead of running the new search. New search, new key.

Daily budget

Every method on this surface, searches and typeaheads alike, spends one shared per-account bucket called scraping.
Default budget: 100 scraping calls per day per connected account, with a 240 second cooldown enforced after every 10 calls. So roughly ten calls back to back (a few typeaheads plus the search), then about four minutes of waiting. When the budget is spent, the account is on hold, or the cooldown has not elapsed, the call returns 429 rate_limited with reason: "bucket_saturated", the cause (daily_saturation, held or delay_not_elapsed) and a retry_after timestamp. Retry after that timestamp, not on a fixed schedule.
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.
  1. Check commercial_use_limit_hit. true on search-people means LinkedIn truncated the page behind its monthly paywall. Re-run on search-sales-nav-people, which has no such limit, or wait for LinkedIn’s monthly reset.
  2. 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.
  3. 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.
  4. Re-read your filter names. A misspelled member is dropped silently, so the search ran less constrained than you meant.
  5. 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 422 before anything runs.
  6. Read the ledger row. status: "completed" with zero rows means LinkedIn genuinely answered with nothing; status: "failed" with an error_code means 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 in error.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.

Where a search shows up in the app

There is no run-now search screen; Data Requests is the receipt. Filter Kind to Scrape, then read Request for the method, Executed on for the account that ran it, and View result payload for the raw response.
Copy this to your AI assistant to page through a search:“Page a LinkedIn people search through the GTM API at https://app.gtm-api.com/linkedin/v4. POST to /api/linkedin-scraping/search-people with header Authorization: Bearer gtm_live_YOUR_KEY. Send either “url” (a pasted /search/results/people/ link) or “filters”, never both, plus “page” and “linkedin_account_sid”: “ln_ac_YOUR_ACCOUNT”. Start at page 1 and re-call with page + 1 while result.paging.has_more is true, stopping at page 100. Use a distinct idempotency_key per page. On 429, read error.context.retry_after and wait until that timestamp. Log result.commercial_use_limit_hit. Write rows to CSV: ln_member_id, nickname, full_name.”