# /linkedin/search/sql/users

`POST /api/linkedin/search/sql/users`

Price: 20 credits

Find LinkedIn people by role, seniority, company, industry, company size, location, skills and education. Built for ICP discovery, buying-committee mapping, alumni lists and lookalikes.

## How to use it

Combine `keywords` with at least one selective filter — it only helps for distinctive words. Most attributes here are sparse, so a filter on one also means 'and this person states it' — each field says how sparse it is, the `has_*` flags make the narrowing explicit, and `dry_run` sizes a filter set before it is run for real. The badge and engagement filters never combine with `similar_to`/`also_viewed`: no record carries both, so any pairing of the two returns nothing. When more people match than `count`, the answer is an unbiased sample of them and repeating the request returns the same people, so narrow the filters or walk the rest with `bucket_total` and `bucket_index` rather than calling again for more.

## Working with /linkedin/search

Search LinkedIn users, companies, jobs, posts by keywords and filters. URN filters (current_company, location, industry) accept raw text — backend auto-resolves.

- keywords → /linkedin/search/users → list of LinkedinSearchUser with URN
- keywords → /linkedin/search/companies → list of LinkedinSearchCompany with URN
- company URN from /linkedin/search/companies → current_company filter in /linkedin/search/users

## Parameters

- `access-token` (string, required)

## Request body

- `timeout` (integer) — Max scrapping execution timeout (in seconds) (default: 300; min: 20; max: 1500)
- `keywords` (string) — Token search across name, headline, summary, every role title, every company name and the companies' speciality lists. Matches whole words, so a common one matches most people — always combine it with at least one selective filter. (default: "")
- `name` (string) — Search in `name`. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `headline` (string) — Search in `headline`. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `summary` (string) — Search in the profile summary. Stated by a minority of people, so the filter also drops everyone who states nothing. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `alias` (array) — Batch lookup by exact LinkedIn slug. (default: [])
- `urn` (array) — Batch lookup by exact profile URN. All three forms this platform prints are accepted: 'fsd_profile:ACoAA…', 'urn:li:fsd_profile:ACoAA…', and the bare 'ACoAA…'. A value carrying a URL path, whitespace, or another type's prefix — a member URN, say — is rejected with a 422 naming the field to use. A bare token is read as a profile id whatever it looks like, so a public slug sent here is NOT rejected: it simply matches nobody. Slugs belong in `alias`, member ids in `member_id`. A URN that matches nobody comes back as an empty list, not an error, so an id that is merely absent is indistinguishable from one that is wrong or sent to the wrong field. (default: [])
- `member_id` (array) — Batch lookup by member id. All three spellings are accepted: 'member:115562011', 'urn:li:member:115562011', and the bare '115562011'. A value carrying a URL path, whitespace, or another type's prefix — a profile URN, say — is rejected with a 422 naming the field to use. A bare token is read as a member id whatever it looks like, so a public slug sent here is NOT rejected: it simply matches nobody. Slugs belong in `alias`, profile URNs in `urn`. A member id that matches nobody comes back as an empty list, not an error, so an id that is merely absent is indistinguishable from one sent to the wrong field. (default: [])
- `last_name` (array) — Exact surname, matched on a folded key: diacritics, case, punctuation and spacing are ignored, so 'van der Berg', 'Van Der Berg' and 'vanderberg' are one value. (default: [])
- `first_initial` (array) — First letter of the given name, lowercased. Pair with `last_name` for 'J. Smith'. (default: [])
- `current_title` (string) — Search in the current role title. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `any_title` (string) — Search across ALL role titles, past included. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `seniority` (array) — Current seniority. One of ['unknown', 'entry', 'ic', 'senior_ic', 'manager', 'head', 'vp', 'founder', 'cxo']. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit. (default: [])
- `seniority_min` (string, nullable) — Current seniority at or above this level — 'head' means Head, VP, founder, CXO. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit.
- `function` (array) — Current function. One of ['unknown', 'other', 'none', 'sales', 'marketing', 'engineering', 'product', 'data', 'finance', 'hr', 'ops', 'legal', 'support', 'exec', 'education', 'healthcare', 'trades', 'admin']. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit. (default: [])
- `any_function` (array) — Function held in ANY role, past included. (default: [])
- `current_company_name` (string) — Search in the current company. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `current_company_id` (array) — Exact LinkedIn numeric company ids. Read from the current role, and only from what that role states about the employer, so it answers for about one person in five — narrower than `has_current_role`, which gates the role itself and not the employer's details. (default: [])
- `current_company_domain` (array) — Bare domains, e.g. ['acme.com']. Scheme, `www.` and path are stripped on both sides. Read from the current role, and only from what that role states about the employer, so it answers for about one person in five — narrower than `has_current_role`, which gates the role itself and not the employer's details. (default: [])
- `any_company_id` (array) — Worked there at any point, current or past. (default: [])
- `past_company_id` (array) — Worked there and has since left (S11 alumni). (default: [])
- `any_company_domain` (array) — Worked at a company on this domain at any point, current or past. (default: [])
- `industry` (array) — Current company industry. Either a taxonomy label or an industry URN — `software development`, `industry:4` and `4` all resolve to the same value, case-insensitively; a URN of any other type is rejected by name. A value this deployment cannot search is rejected with 422 naming the closest labels it can, so a typo never reads as an empty market. The vocabulary is LinkedIn's published industry-codes-v2 table, but only the part indexed here, which is narrower than what LinkedIn shows on a profile: labels outside it — finance, education, construction and hospitality among them — are stored as company keywords, so no filter reaches them yet. `linkedin/search/industries` resolves a name against LinkedIn live and can therefore return a URN this filter still rejects. Stated for roughly one person in eight — `has_industry` gates on ANY company, so it does not guarantee a match here; `any_industry` is the filter it belongs to. (default: []; examples: ["software development","industry:96"])
- `any_industry` (array) — Industry of ANY company worked at, past included — 'has worked in this sector'. Either a taxonomy label or an industry URN — `software development`, `industry:4` and `4` all resolve to the same value, case-insensitively; a URN of any other type is rejected by name. A value this deployment cannot search is rejected with 422 naming the closest labels it can, so a typo never reads as an empty market. The vocabulary is LinkedIn's published industry-codes-v2 table, but only the part indexed here, which is narrower than what LinkedIn shows on a profile: labels outside it — finance, education, construction and hospitality among them — are stored as company keywords, so no filter reaches them yet. `linkedin/search/industries` resolves a name against LinkedIn live and can therefore return a URN this filter still rejects. Stated by a minority of people, so the filter also drops everyone who states nothing. Gate it with `has_industry`. (default: []; examples: ["it services and it consulting"])
- `employee_range` (array) — Company size band of the current company, one of ['0-1', 'myself only', '2-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5001-10000', '10001+']; anything else is rejected with 422 rather than silently matching nobody. A profile prints the two edge bands as the bare '1' and '10001', and both are accepted here and read as 'myself only' and '10001+'. Read from the current role, and only from what that role states about the employer, so it answers for about one person in five — narrower than `has_current_role`, which gates the role itself and not the employer's details. `has_company_size` gates on ANY company, so it does not guarantee a match here. (default: []; examples: ["1001-5000","10001+"])
- `employee_count_min` (integer, nullable) — Min current company headcount. Read from the current role, and only from what that role states about the employer, so it answers for about one person in five — narrower than `has_current_role`, which gates the role itself and not the employer's details.
- `employee_count_max` (integer, nullable) — Max current company headcount. Read from the current role, and only from what that role states about the employer, so it answers for about one person in five — narrower than `has_current_role`, which gates the role itself and not the employer's details.
- `country` (array) — ISO2 country codes, e.g. ['US','GB']. Use this for a country, and add `location` alongside it for a place inside one, such as a city or a region. (default: [])
- `location` (string) — Search in the free-text location — a city, a region, a metro area. For a whole country use `country`. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `skills` (string) — Search the skills list. Stated by a minority of people, so the filter also drops everyone who states nothing. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `languages` (string) — Search the languages list. Stated by a minority of people, so the filter also drops everyone who states nothing. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `edu_slug` (array) — Exact institution slugs, e.g. ['tu-delft']. (default: [])
- `edu_institution` (string) — Search institution names. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `edu_field` (string) — Search field of study / degree text. Mini-DSL: whitespace=AND, `|`=OR (no spaces around `|`), `"phrase"`=exact phrase, `-token`=NOT. Example: `sales|revenue "account executive" -intern`. (default: "")
- `edu_ended_year_min` (integer, nullable) — Graduated in or after this year.
- `edu_ended_year_max` (integer, nullable) — Graduated in or before this year.
- `months_in_role_max` (integer, nullable) — New in role: started at most this many months ago. Measured from today. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit. (min: 0)
- `months_in_role_min` (integer, nullable) — In the current role at least this long. Read from the current role, which resolves for about a third of people — `has_current_role` makes that narrowing explicit. (min: 0)
- `experience_years_min` (integer, nullable) — First recorded role is at least this old. (min: 0)
- `months_since_change_max` (integer, nullable) — Last started or ended a role at most this many months ago. (min: 0)
- `n_roles_min` (integer, nullable) — At least this many recorded roles. (min: 0)
- `n_roles_max` (integer, nullable) — At most this many recorded roles. (min: 0)
- `n_companies_min` (integer, nullable) — Worked at at least this many companies. (min: 0)
- `n_companies_max` (integer, nullable) — At most this many companies — stability, not job-hopping. (min: 0)
- `promotion_count_min` (integer, nullable) — Changed title at least this many times without changing employer. Stated by a minority of people, so the filter also drops everyone who states nothing. It needs two dated roles at one employer, which is why it is the narrowest of the tenure filters. (min: 0)
- `avg_tenure_months_min` (integer, nullable) — Mean length of the finished roles, in months. Ignores the current one. (min: 0)
- `avg_tenure_months_max` (integer, nullable) — Upper bound on the same mean. (min: 0)
- `profile_score_min` (integer, nullable) — How complete the profile is: one point each for roles, education, skills, headline, summary, photo, country and industry. Use it to demand rich profiles. (min: 0; max: 8)
- `is_creator` (boolean, nullable) — Creator badge. Not carried by the response. Only a freshly scraped profile carries this flag at all; on everyone else it is unset, and unset matches neither setting. So `false` is not the complement of `true` — both narrow to the same small population, and `false` is no way to exclude the badge.
- `is_influencer` (boolean, nullable) — Influencer badge. Not carried by the response. Only a freshly scraped profile carries this flag at all; on everyone else it is unset, and unset matches neither setting. So `false` is not the complement of `true` — both narrow to the same small population, and `false` is no way to exclude the badge. It is also the rarest badge by a wide margin — expect a handful of people, not a list.
- `is_top_voice` (boolean, nullable) — Top Voice badge. Only a freshly scraped profile can carry the badge; everywhere else it is recorded as `false`. So `true` narrows hard, and `false` excludes almost nobody.
- `is_premium` (boolean, nullable) — Premium subscriber. Only a freshly scraped profile can carry the badge; everywhere else it is recorded as `false`. So `true` narrows hard, and `false` excludes almost nobody.
- `open_to_work` (boolean, nullable) — Open-to-work badge. `false` means no badge was observed, which is not the same as the person not looking. Only a freshly scraped profile can carry the badge; everywhere else it is recorded as `false`. So `true` narrows hard, and `false` excludes almost nobody.
- `hiring` (boolean, nullable) — Hiring badge. Same caveat as `open_to_work`. Only a freshly scraped profile can carry the badge; everywhere else it is recorded as `false`. So `true` narrows hard, and `false` excludes almost nobody.
- `verified` (boolean, nullable) — Identity-verified. Only a freshly scraped profile can carry the badge; everywhere else it is recorded as `false`. So `true` narrows hard, and `false` excludes almost nobody.
- `follower_count_min` (integer, nullable) — At least this many followers. Only a small share of profiles state one, and it is the same freshly scraped population that carries the badges — gate it with `has_engagement`.
- `connection_count_min` (integer, nullable) — At least this many connections. Stated about as rarely as the follower count, and by the same people — gate it with `has_engagement`.
- `similar_to` (array) — Aliases whose 'similar profiles' graph should be searched (lookalike seed). Stated by a minority of people, so the filter also drops everyone who states nothing. It is also the mirror image of the badge and engagement filters: no record carries both, so pairing this with one of those matches nobody at all. (default: [])
- `also_viewed` (array) — Aliases from the 'people also viewed' graph — a looser lookalike than `similar_to`. Stated by a minority of people, so the filter also drops everyone who states nothing. Same mutual exclusion with the badge and engagement filters. (default: [])
- `has_current_role` (boolean, nullable) — Only people whose current employer can be resolved — a role with a start and no end. Left unset, people without one are included.
- `has_industry` (boolean, nullable) — Only people where SOME company on the profile states an industry, past employers included. It is the gate for `any_industry`; the current company states one about half as often, so it does not guarantee a match on `industry`.
- `has_company_size` (boolean, nullable) — Only people where SOME company on the profile states a size, past employers included. The current company states one about half as often, so it does not guarantee a match on `employee_range` or `employee_count_min`/`max`.
- `has_education` (boolean, nullable) — Only people stating an education entry.
- `has_roles` (boolean, nullable) — Only people stating any role history.
- `has_role_dates` (boolean, nullable) — Only people with at least one dated role — required for the tenure filters.
- `has_engagement` (boolean, nullable) — Only people stating a follower count or a creator badge — well under one person in a hundred, the narrowest gate here. Set it before using the engagement or badge filters so the narrowing is deliberate rather than accidental.
- `fetched_after` (integer, nullable) — Only people whose profile was refreshed at or after this unix timestamp.
- `bucket_total` (integer, nullable) — Divide the matching people into this many equal parts, so a team can split a territory without overlap. The same person always lands in the same part. (min: 2; max: 100)
- `bucket_index` (integer, nullable) — Which part to return, counting from zero. (min: 0)
- `count` (integer, required) — Max result count. A `dry_run` request ignores it. (max: 1000; >: 0)
- `dry_run` (boolean) — Return how many people match these filters instead of the people. The response is a 209 with an empty body and the number in the headers, so a filter set can be sized before it is run for real. It counts distinct people, the same unit a normal call returns, and stops at 100000 — a wider filter set reports exactly that and means 'at least'. (default: false)

## Response

### 200 — Successful Response

- `@type` (string) (default: "LinkedinUser")
- `internal_id` (object, nullable)
  - `type` (string, required) (one of: "fsd_company", "fsd_group", "fsd_profile", "fsd_skill", "activity", "comment", "company", "group", "geo", "pulse", "member", "industry", "ugcPost", "job", "message", "conversation", "media", "share", "linkedInArticle")
  - `value` (string, required)
- `urn` (object, nullable)
  - `type` (string, required) (one of: "fsd_company", "fsd_group", "fsd_profile", "fsd_skill", "activity", "comment", "company", "group", "geo", "pulse", "member", "industry", "ugcPost", "job", "message", "conversation", "media", "share", "linkedInArticle")
  - `value` (string, required)
- `name` (string, required)
- `first_name` (string, nullable)
- `last_name` (string, nullable)
- `alias` (string, required)
- `url` (string, required)
- `headline` (string, nullable)
- `headlines` (array, nullable)
  - `@type` (string) (default: "LinkedinUserHeadline")
  - `locale` (string, required)
  - `text` (string, required)
  - `is_primary` (boolean) (default: false)
- `follower_count` (integer, nullable)
- `image` (string, nullable)
- `background_image` (string, nullable)
- `connection_count` (integer, nullable)
- `description` (string, nullable)
- `top_skills` (array, nullable)
- `frame` (string, nullable) (one of: "open_to_work", "hiring")
- `location` (string, nullable)
- `country_code` (string, nullable)
- `country` (string, nullable)
- `city` (string, nullable)
- `pronouns` (string, nullable)
- `custom_pronouns` (string, nullable)
- `experience` (array, nullable)
  - `@type` (string) (default: "LinkedinUserExperience")
  - `company` (object, required)
    - `@type` (string) (default: "LinkedinUserExperienceCompany")
    - `name` (string, required)
    - `urn` (object, nullable)
      - `type` (object, required) (see schema LinkedinURNPrefix)
      - `value` (string, required)
    - `url` (string, nullable)
    - `alias` (string, nullable)
    - `industry` (object, nullable)
      - `type` (string, required) (one of: "industry")
      - `value` (string, required)
    - `industry_full` (object, nullable)
      - `@type` (string) (default: "LinkedinIndustry")
      - `urn` (object, required) (see schema LinkedinURN_Literal_industry__)
      - `name` (string, required)
      - `hierarchy` (string, required)
      - `description` (string, required)
    - `employee_range` (string, nullable)
    - `logo_url` (string, nullable)
  - `position` (string, nullable)
  - `work_type` (string, nullable) (one of: "on-site", "hybrid", "remote")
  - `employment` (string, nullable) (one of: "full-time", "part-time", "permanent", "self-employed", "freelance", "contract", "internship", "apprenticeship", "indirect contract")
  - `interval` (string, nullable)
  - `start_date` (string, nullable) — Role start, `YYYY-MM` or `YYYY`.
  - `end_date` (string, nullable) — Role end, `YYYY-MM` or `YYYY`. Absent while ongoing.
  - `period` (string, nullable)
  - `location` (string, nullable)
  - `description` (string, nullable)
  - `medias` (array, nullable)
    - `@type` (string) (default: "LinkedinUserExperienceMedia")
    - `id` (string, nullable)
    - `url` (string, nullable)
    - `text` (string, nullable)
    - `image` (string, nullable)
- `education` (array, nullable)
  - `@type` (string) (default: "LinkedinUserEducation")
  - `company` (object, required)
    - `@type` (string) (default: "LinkedinUserEducationCompany")
    - `name` (string, required)
    - `urn` (object, nullable)
      - `type` (object, required) (see schema LinkedinURNPrefix)
      - `value` (string, required)
    - `url` (string, nullable)
    - `alias` (string, nullable)
    - `industry` (object, nullable)
      - `type` (string, required) (one of: "industry")
      - `value` (string, required)
    - `industry_full` (object, nullable)
      - `@type` (string) (default: "LinkedinIndustry")
      - `urn` (object, required) (see schema LinkedinURN_Literal_industry__)
      - `name` (string, required)
      - `hierarchy` (string, required)
      - `description` (string, required)
    - `employee_range` (string, nullable)
    - `logo_url` (string, nullable)
  - `major` (string, nullable)
  - `interval` (string, nullable)
  - `description` (string, nullable)
- `languages` (array, nullable)
  - `@type` (string) (default: "LinkedinUserLanguage")
  - `name` (string, required)
  - `level` (string, nullable)
- `certificates` (array, nullable)
  - `@type` (string) (default: "LinkedinUserCertificate")
  - `name` (string, required)
  - `company` (object, nullable)
    - `@type` (string) (default: "LinkedinUserCertificateCompany")
    - `name` (string, required)
    - `urn` (object, nullable)
      - `type` (string, required) (one of: "fsd_company", "company")
      - `value` (string, required)
    - `url` (string, nullable)
  - `created_at` (string, nullable)
  - `label` (string, nullable)
  - `url` (string, nullable)
- `honors` (array, nullable)
  - `@type` (string) (default: "LinkedinUserHonor")
  - `name` (string, required)
  - `issued_by` (string, nullable)
  - `issued_at` (string, nullable)
  - `text` (string, nullable)
- `patents` (array, nullable)
  - `@type` (string) (default: "LinkedinUserPatent")
  - `name` (string, required)
  - `id` (string, nullable)
  - `issued_at` (string, nullable)
  - `text` (string, nullable)
  - `url` (string, nullable)
  - `inventor_count` (integer, nullable)
- `skills` (array, nullable)
  - `@type` (string) (default: "LinkedinUserSkill")
  - `name` (string, required)
  - `urn` (object, nullable)
    - `type` (string, required) (one of: "fsd_company", "fsd_group", "fsd_profile", "fsd_skill", "activity", "comment", "company", "group", "geo", "pulse", "member", "industry", "ugcPost", "job", "message", "conversation", "media", "share", "linkedInArticle")
    - `value` (string, required)
- `verified` (boolean) (default: false)
- `is_premium` (boolean) (default: false)
- `is_top_voice` (boolean) (default: false)
- `fetched_at` (integer, nullable)

### 209

Nobody matched these filters, or the request was a `dry_run`.

## Errors

### 422 — Validation Error

The request body did not validate

What to do: Check the fields against this schema. A URN with the wrong prefix is the most common cause.

- `detail` (array)
  - `loc` (array, required)
  - `msg` (string, required)
  - `type` (string, required)
  - `input` (any)
  - `ctx` (object)

### 408

The request ran past its time limit

What to do: Raise `timeout` in the request body, up to the maximum this endpoint documents. Lowering `count` or turning off the `with_*` flags also helps, because less work finishes sooner.

### 412

The entity was not found, or a precondition failed

What to do: Retrying will not help: either the entity does not exist, or the input points at a different one.

### 429

Too many requests: a rate limit or a usage window is exhausted

What to do: When the response carries an X-Retry-After header, wait that many seconds and retry: the same number is in the body as `detail.retry_after`, and the limit clears once that window passes. The message in the body names the limit that was hit.

### 500

Something broke on our side

What to do: Retrying will not help. If it keeps happening, send us the X-Request-ID from the response headers.

### 529

Rate limit reached, or the endpoint is overloaded

What to do: Wait at least 30 seconds, then retry.

## Response envelope

Success: Array of objects (may be empty if no results)

Error: Error may coexist with partial results if it occurs mid-execution. Check X-Error header and status code.

Every response carries these headers:

- `X-Error` — Error message text (present only on error)
- `X-Request-ID` — Unique request identifier
- `X-Execution-Time` — Execution time in seconds
- `X-Result-Count` — How many records the body carries. 0 means an empty result, which is a normal answer and not by itself an error. A non-zero count can come back together with X-Error when the failure happened partway through — read this header and X-Error independently.
- `X-Total-Available-Results` — How many records exist for this query, when the endpoint can say. On a `dry_run` request this is the answer and the body is empty. It saturates: the endpoint's documented maximum means 'at least that many', any smaller number is exact.
- `X-Warning` — Present when the request body carried keys this endpoint does not document. They were ignored, so any filter you meant to apply through them did not apply. Check the spelling against this schema and retry.
- `X-Retry-After` — Seconds to wait before retrying. Present only on 429.

