{
  "data": {
    "id": "JLGCe3YB7xfqhWxBd",
    "actId": "fulnoB4QHwZkIb9iM",
    "userId": "WudrkJ1Wey1B2q4SR",
    "startedAt": "2026-08-20T05:12:37.410Z",
    "finishedAt": "2026-08-20T05:12:43.077Z",
    "status": "SUCCEEDED",
    "meta": {
      "origin": "CLI"
    },
    "stats": {
      "durationMillis": 5545,
      "runTimeSecs": 5.545,
      "computeUnits": 0.006161111111111111,
      "imageSizeBytes": 579710464
    },
    "options": {
      "useCache": true,
      "betaPackages": false,
      "memoryMbytes": 4096,
      "diskMbytes": 8192
    },
    "exitCode": 0,
    "inputSchema": "{\n  \"title\": \"LinkedIn Profile Scraper\",\n  \"description\": \"Scrape complete LinkedIn profiles from URLs, usernames, or profile IDs — work history, education, skills, current-company details and optional business-email enrichment. Optional filters narrow the profiles you supply. No LinkedIn login, cookies, li_at or session ID is ever required.\",\n  \"type\": \"object\",\n  \"schemaVersion\": 1,\n  \"properties\": {\n    \"profiles\": {\n      \"title\": \"LinkedIn profiles\",\n      \"type\": \"array\",\n      \"description\": \"Paste any mix of full profile URLs (\\\"https://www.linkedin.com/in/example-user/\\\"), usernames / public identifiers (\\\"example-user\\\"), or LinkedIn internal profile IDs (\\\"ACoAAA8BYqEBCGLg...\\\"). The type of each entry is detected automatically, equivalent entries are merged, and malformed entries are reported and skipped without stopping the run.\\n\\nURLs and usernames are read from LinkedIn's public profile pages, which costs nothing upstream. Profile IDs are not resolvable as URLs, so those always use the metered provider.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": [\n        \"https://www.linkedin.com/in/williamhgates/\"\n      ],\n      \"sectionCaption\": \"LinkedIn Profiles\",\n      \"sectionDescription\": \"The people to scrape. This is the only input that finds anyone — the actor looks up profiles you name, it does not search LinkedIn for new people. Filters below narrow this list.\"\n    },\n    \"maxProfiles\": {\n      \"title\": \"Maximum profiles\",\n      \"type\": \"integer\",\n      \"description\": \"Maximum number of unique people to save. A global run limit. Duplicates and profiles removed by filters do not count towards it and are never charged.\",\n      \"minimum\": 1,\n      \"maximum\": 5000,\n      \"default\": 100,\n      \"prefill\": 100,\n      \"unit\": \"profiles\"\n    },\n    \"currentJobTitles\": {\n      \"title\": \"Current job titles\",\n      \"type\": \"array\",\n      \"description\": \"Keep only people whose CURRENT title matches any of these, matched on title and headline.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": [],\n      \"sectionCaption\": \"Filters\",\n      \"sectionDescription\": \"All optional. Each filter narrows the profiles you supplied above — none of them find new people. Filters that need work history or education require Profile data level \\\"Full profile\\\" or higher; the run warns you if you set one that cannot be evaluated.\"\n    },\n    \"locations\": {\n      \"title\": \"Locations\",\n      \"type\": \"array\",\n      \"description\": \"Keep only people in any of these locations, matched against the location text on their profile.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"pastJobTitles\": {\n      \"title\": \"Past job titles\",\n      \"type\": \"array\",\n      \"description\": \"Keep only people who previously held any of these titles, matched against their work history. Requires Full profile data.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"pastCompanies\": {\n      \"title\": \"Past companies\",\n      \"type\": \"array\",\n      \"description\": \"Keep only people who previously worked at any of these companies, matched against their work history. Requires Full profile data.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"industries\": {\n      \"title\": \"Industries\",\n      \"type\": \"array\",\n      \"description\": \"Keep only people whose current employer is in any of these industries. Requires Include current company details.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"schools\": {\n      \"title\": \"Schools\",\n      \"type\": \"array\",\n      \"description\": \"Match people who attended any of these schools. Accepts school or university names, LinkedIn school URLs, or school IDs. Where LinkedIn cannot filter on this directly it is applied after extraction and requires Full profile data.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"minimumYearsExperience\": {\n      \"title\": \"Minimum years of experience\",\n      \"type\": \"integer\",\n      \"description\": \"Only keep people with at least this much total work experience. Computed from the work history, counting overlapping roles once. Requires Full profile data — in Basic mode there is no work history to measure, so this filter is ignored and the run warns you.\",\n      \"minimum\": 0,\n      \"maximum\": 60,\n      \"unit\": \"years\",\n      \"nullable\": true\n    },\n    \"maximumYearsExperience\": {\n      \"title\": \"Maximum years of experience\",\n      \"type\": \"integer\",\n      \"description\": \"Only keep people with at most this much total work experience. Requires Full profile data; ignored in Basic mode.\",\n      \"minimum\": 0,\n      \"maximum\": 60,\n      \"unit\": \"years\",\n      \"nullable\": true\n    },\n    \"minimumYearsAtCurrentCompany\": {\n      \"title\": \"Minimum years at current company\",\n      \"type\": \"integer\",\n      \"description\": \"Only keep people who have been at their current company for at least this long. Requires Full profile data, because the current role's start date is not in the search results; ignored in Basic mode.\",\n      \"minimum\": 0,\n      \"maximum\": 60,\n      \"unit\": \"years\",\n      \"nullable\": true\n    },\n    \"maximumYearsAtCurrentCompany\": {\n      \"title\": \"Maximum years at current company\",\n      \"type\": \"integer\",\n      \"description\": \"Only keep people who have been at their current company for at most this long — useful for finding recent movers. Requires Full profile data; ignored in Basic mode.\",\n      \"minimum\": 0,\n      \"maximum\": 60,\n      \"unit\": \"years\",\n      \"nullable\": true\n    },\n    \"includeKeywords\": {\n      \"title\": \"Must contain keywords\",\n      \"type\": \"array\",\n      \"description\": \"Only keep people whose profile text contains at least one of these. Matched case-insensitively against the headline, about section, current title, skills and work-history titles. A person whose profile could not be read at all is kept rather than dropped.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"excludeKeywords\": {\n      \"title\": \"Exclude keywords\",\n      \"type\": \"array\",\n      \"description\": \"Drop people whose profile text contains any of these. Applied conservatively — matched on whole words, so excluding 'AI' does not drop someone called 'Said'.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"excludeCurrentCompanies\": {\n      \"title\": \"Exclude current companies\",\n      \"type\": \"array\",\n      \"description\": \"Drop people who currently work at any of these companies. Accepts names, LinkedIn company URLs, or company IDs. Applied before any profile page is opened, so excluded people cost nothing.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"excludeJobTitles\": {\n      \"title\": \"Exclude job titles\",\n      \"type\": \"array\",\n      \"description\": \"Drop people whose current title contains any of these words — for example 'Recruiter', 'Talent Acquisition'. Matched case-insensitively, and applied before any profile page is opened, so excluded people cost nothing.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"includeCurrentCompanyDetails\": {\n      \"title\": \"Include current company details\",\n      \"type\": \"boolean\",\n      \"description\": \"Add the person's current employer: website, domain, industry, company size, employee count, company type, headquarters, offices, founding year, description and logo. Each unique company is looked up once per run and charged once — 100 people at one company is a single company charge, not 100. Failed or blocked lookups return the person without company data and are never charged.\",\n      \"default\": false,\n      \"prefill\": false,\n      \"sectionCaption\": \"Enrichment\",\n      \"sectionDescription\": \"Optional extras, each billed as its own event so you only pay for what you turn on.\"\n    },\n    \"findEmail\": {\n      \"title\": \"Find business email\",\n      \"type\": \"boolean\",\n      \"description\": \"Extract a business email address for each unique person. Two sources, and the output records which via contact.source.\\n\\nFREE: an address published in the public LinkedIn text this Actor already reads — their About section or their employer's description. Costs nothing and is never charged. Most people publish no address.\\n\\nPAID (requires a PAID Apify plan — on the Free plan this step is skipped entirely, nothing is charged, and the free extraction above still runs): the person's name and their employer's mail domain are sent to a third-party email finder. That provider is NOT LinkedIn, and such an address is labelled contact.source 'enrichment'. You are charged ONLY when the provider returns an address it has CONFIRMED to be deliverable.\\n\\nUNCONFIRMED ADDRESSES ARE DISCARDED, NOT RETURNED. Many large employers run mail gateways that accept every recipient, so no provider can confirm whether a mailbox exists there — and a provider asked about such a domain will return a pattern-derived address for any name at all, including one belonging to nobody. Those are dropped rather than handed to you, and never charged. The consequence is real: expect an empty email column for a substantial share of people at large enterprises, by design.\\n\\nEach unique person is looked up at most once per run. Duplicates, filtered profiles and skip-list matches are never looked up. People whose employer domain we could not resolve cannot be looked up at all — the run log reports how many.\",\n      \"default\": false,\n      \"prefill\": false\n    },\n    \"removeDuplicates\": {\n      \"title\": \"Remove duplicate people\",\n      \"type\": \"boolean\",\n      \"description\": \"Return each person once, even when several searches find them or you also supply their profile directly. Matched on LinkedIn profile ID first, then public identifier, then profile URL. Every search that matched a person is preserved in matchedSearches, and merging never causes a second charge.\",\n      \"default\": true,\n      \"prefill\": true,\n      \"sectionCaption\": \"Deduplication & Incremental\",\n      \"sectionDescription\": \"Control repeats within this run, and skip people you already collected in earlier runs.\"\n    },\n    \"skipProfileIds\": {\n      \"title\": \"Skip profile IDs\",\n      \"type\": \"array\",\n      \"description\": \"LinkedIn internal profile IDs to skip — for example 'ACoAAA123...'. Matched people are not extracted, not enriched, not saved and never charged. Entries are cross-matched against all three skip lists, so a URL pasted here still works.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"skipPublicIdentifiers\": {\n      \"title\": \"Skip usernames\",\n      \"type\": \"array\",\n      \"description\": \"Public identifiers / usernames to skip — for example 'john-smith'. Matched people are never extracted, enriched, saved or charged.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"skipProfileUrls\": {\n      \"title\": \"Skip profile URLs\",\n      \"type\": \"array\",\n      \"description\": \"Full LinkedIn profile URLs to skip. Matched people are never extracted, enriched, saved or charged.\",\n      \"editor\": \"stringList\",\n      \"default\": [],\n      \"prefill\": []\n    },\n    \"proxyConfiguration\": {\n      \"title\": \"Proxy configuration\",\n      \"type\": \"object\",\n      \"description\": \"Apify Datacenter proxy is the default, and is what this Actor uses.\\n\\nRESIDENTIAL IS NOT USED, even if you select it. LinkedIn blocks a large share of public profile requests regardless of proxy type: measured across datacenter, residential, and residential with a regenerated browser fingerprint, the identical set of profiles was blocked every time — LinkedIn decides per profile, not per requester. Selecting residential would therefore cost bandwidth without changing a single result, so the Actor falls back to datacenter and says so in the log.\\n\\nBlocked profiles still produce a result, recovered through a paid data provider, though that version carries fewer sections.\\n\\nCUSTOM PROXY URLS: if you supply your own provider it is used in full, through your own proxy account. That is the supported way to control which IPs are used.\\n\\nNO PROXY: honoured as-is.\",\n      \"editor\": \"proxy\",\n      \"default\": {\n        \"useApifyProxy\": true\n      },\n      \"prefill\": {\n        \"useApifyProxy\": true\n      },\n      \"sectionCaption\": \"Proxy\"\n    }\n  },\n  \"required\": []\n}",
    "readme": "# LinkedIn Profile Scraper\n\n![LinkedIn Profile Scraper](https://raw.githubusercontent.com/coregentdevspace/linkedin-people-search-profile-scraper-assets/main/thumbnail-linkedin-people-search-profile-scraper.png)\n\nTurn a list of LinkedIn profile URLs, usernames, or profile IDs into structured records —\nverified current employer, full company details, location, and about text.\n\n**No LinkedIn login, cookies, `li_at`, or session ID is ever required.**\n\n> **What you reliably get, and what you do not.** This actor reads LinkedIn's *public,*\n> *logged-out* pages. Name, employer, location, education and photo come back on virtually every\n> profile; certifications and languages on most. But LinkedIn withholds job **titles**, **skills**\n> and employment **dates** from logged-out visitors entirely, so those arrive empty rather than\n> invented — and no proxy or setting changes that. Every figure is measured, not estimated:\n> [What the data looks like in practice](#what-the-data-looks-like-in-practice).\n\n## What does this actor do?\n\nYou give it the people you want. It returns one clean, normalized record each — deduplicated,\nwith the current employer resolved to a real LinkedIn company and optionally enriched with that\ncompany's industry, size, website and headquarters.\n\nPaste any mix of full profile URLs, usernames, or LinkedIn internal profile IDs. The actor detects\neach form automatically, merges equivalent entries, deduplicates people supplied more than once, and\nreturns a consistent record for every one — with optional filters to narrow a large input list.\n\nIt reads LinkedIn's **public** profile pages. You never supply a username, password, cookie, `li_at`\ntoken, or session ID.\n\n> **This actor does not search LinkedIn.** It looks up people you name. There is no public LinkedIn\n> people-search endpoint, so finding new people by job title, location, or keyword is not something\n> this actor can do — see [Limitations](#limitations).\n\n## Main use cases\n\n- **Verify and enrich a contact list** — turn profile URLs into confirmed employer, location and\n  company data. This is what the actor does best.\n- **Account mapping** — resolve a list of people to the companies they work at, with industry,\n  size and domain attached.\n- **CRM hygiene** — refresh employers and locations for existing contacts, and spot movers.\n- **Lead enrichment** — add the company domain you need for downstream email tooling.\n- **Automation** — call it from the Apify API, a Task, a Schedule, or a webhook.\n\n## Quick start\n\n```json\n{\n  \"profiles\": [\n    \"https://www.linkedin.com/in/example-user/\",\n    \"another-user\",\n    \"ACoAAA8BYqEBCGLg...\"\n  ],\n  \"profileDataLevel\": \"full\"\n}\n```\n\n## What you get\n\nEvery profile is returned at **full detail** — there is no tier to choose.\n\n| Field | Returned |\n|---|---|\n| Name, location, profile URL | ✅ always |\n| Current employer — name, LinkedIn URL, slug | ✅ always |\n| Profile photo | ✅ always |\n| About / summary | usually — 65% |\n| Education | usually — 77% |\n| Work history (employer; **dates are not published**) | usually — 87% |\n| Languages | often — 50% |\n| Certifications | sometimes — 35% |\n| Volunteering, honours | sometimes — ~20% |\n| Job title, skills, tenure totals | **not published by LinkedIn** — see below |\n| Current company details | opt-in — **Include current company details** |\n| Business email | opt-in — **Find business email** |\n| LinkedIn numeric member ID | when the paid fallback ran |\n| Banner image + default-photo flag | when the paid fallback ran |\n| Influencer / memorialized flags | when the paid fallback ran |\n| \"People also viewed\" — 10 related profiles | when the paid fallback ran |\n\nThose percentages are from a real 52-profile run, not estimates — the full table is in\n[What the data looks like in practice](#what-the-data-looks-like-in-practice). Sections LinkedIn\nwithholds come back empty rather than guessed, and a filter that depends on missing data tells you so\nin the run log instead of silently passing everyone.\n\n**Two of those need a word of explanation.** `hasDefaultProfileImage: true` means\n`profileImageUrl` is LinkedIn's grey placeholder rather than a real photo — the placeholder is a\nperfectly valid image URL, so without the flag you cannot tell them apart. And `linkedinNumericId` is\nLinkedIn's internal member id, stable even when someone changes their profile URL; it is not the\n`ACoAAA…` URN, which this data source does not expose at all.\n\n`peopleAlsoViewed` lists 10 profiles LinkedIn considers related. It is returned as data and is\n**never crawled** — this Actor only fetches people you named, so those entries are a starting point\nfor you rather than rows you will be charged for.\n\nThose five fields come from the paid fallback, which runs when LinkedIn blocks the public page — so in\npractice most rows have them and the few served from the public page do not. They are always present\nas keys, empty where unavailable.\n\n**Why there is no Basic tier.** There used to be, and it was removed because it saved nobody\nanything. Every person you supply has their profile page opened regardless — the basic fields come\nfrom that page — so a Basic row did exactly the same work and then had fields stripped before being\nreturned. It cost us the same to produce and cost you less, which made it the worst possible deal for\nboth sides. Full detail for everyone is simpler and honest.\n\n**One result event per person, and none at all for an empty row.** If LinkedIn does not serve\nsomeone's profile page and the paid fallback cannot recover it either, that person is still returned —\nso you can see exactly who was missed rather than finding a short file — but **no event is charged for\nthem**. You pay for people you actually received data about.\n\n\n## What the data looks like in practice\n\n**Measured on a real 52-profile run**, not estimated — 52 people across 42 employers, 15 industries\nand 17 countries, deliberately spread so no single employer or profession skews the numbers. Every\nrow returned data; 42 came through the paid fallback and 10 from the public page directly.\n\n| Field | Populated | |\n|---|---|---|\n| Profile URL, `publicIdentifier` | **100%** | ✅ |\n| Name, photo, follower count, location | **98%** | ✅ |\n| Current employer (name + LinkedIn URL) | **98%** | ✅ |\n| Work history entries | **87%** | ✅ |\n| Connections count, numeric member ID, banner | **87%** | ✅ |\n| Education | **77%** | ✅ |\n| \"People also viewed\" (10 related profiles) | **77%** | ✅ |\n| About / summary | **65%** | |\n| Languages | **50%** | |\n| **Business email** *(opt-in)* | **45%** of people with a company domain — **40%** of all rows | |\n| Certifications | **35%** | |\n| Volunteering | 21% | |\n| Honours & awards | 19% | |\n| Other organisations / affiliations | 17% | |\n| Websites | 14% | |\n| Courses | 10% | |\n| Recommendations count | 8% | |\n| Headline, job **title**, years of experience / tenure | 6% | ⚠️ |\n| Projects | 4% | |\n| Skills, publications | **0%** | ⚠️ |\n| Open-to-work / hiring badges | **0%** | ⚠️ |\n| LinkedIn URN (`profileId`) | **0%** | ⚠️ |\n\n> **These figures replaced ones measured on 18 profiles who were all pharmacists.** Two moved\n> sharply: certifications 72% → 35% and affiliations 72% → 17%. Licensed professionals carry\n> credentials that a general population does not, so the old numbers flattered those two fields.\n> Everything else moved a few points at most.\n\n### The ⚠️ rows, explained\n\nThese are not bugs and no setting changes them. They are what LinkedIn does not publish to a\nlogged-out visitor, and what our data source does not carry.\n\n**Job titles are redacted.** LinkedIn returns them as asterisks on ordinary profiles. We treat a\nredacted value as absent rather than emitting `\"****\"` as though it were an answer — so\n`currentPosition.title` is `null` while `companyName` is populated. **Headline** is empty for the\nsame reason on most profiles.\n\n**Employment arrives without dates.** The employer is published; the start and end dates are not.\nSo `experience[]` is populated while `totalYearsExperience` and `yearsAtCurrentCompany` stay\n`null` — there is nothing to compute them from. `experienceCount: 1` beside\n`totalYearsExperience: null` is honest, not broken.\n\n**Skills and publications** are absent from the data source entirely. Verified against a captured\npayload rather than assumed.\n\n**Projects, recommendations, job title and tenure are rare rather than impossible** — 4%, 8%, 6% and\n6%. They come only from the minority of profiles LinkedIn serves in full to a logged-out visitor, so\nexpect them occasionally and never depend on them.\n\n**`profileId`** is the internal `ACoAAA…` URN, which our source does not expose. Use\n`publicIdentifier` (100%) or `linkedinNumericId` (87%) instead — the latter is stable even if\nsomeone changes their profile URL.\n\n**If your use case needs a job title, dated work history, or skills for every person**, this Actor\ncannot deliver that from public LinkedIn data, and neither can any proxy or setting.\n\n\n## Filters\n\nAll optional, and all applied **after** extraction to narrow the list you supplied. They never find\nnew people.\n\n| Filter | Matched against | Needs Full? |\n|---|---|---|\n| Current job titles | title + headline | no |\n| Locations | the profile's location text | no |\n| Must contain / exclude keywords | headline, about, title, skills, work history | beyond the headline, yes |\n| Exclude job titles / companies | title, headline, current employer | no |\n| Past job titles / companies | work history | **yes** |\n| Schools | education | **yes** |\n| Industries | the enriched company's industry | needs company enrichment |\n| Min/max years of experience | work history, overlapping roles counted once | **yes** |\n| Min/max years at current company | the current role's start date | **yes** |\n\nA filter that cannot be evaluated **keeps** the person rather than dropping them, so nobody is\nsilently discarded because a field failed to parse.\n\n**The run tells you when this happens.** If a filter could not be evaluated for some of your\nresults, the log says so with the count and the reason — for example *\"the Minimum years of\nexperience filter could not be evaluated for 9 of 10 saved profiles (90%), because LinkedIn does\nnot publish dated work history for them. Those people were KEPT rather than dropped, so this\nfilter excluded nobody among them.\"* The same counts appear in `RUN_SUMMARY`.\n\nThat matters most for the four experience and tenure filters, which need dated work history —\navailable for only a minority of profiles. They are genuinely useful when filtering a list of\npeople who do publish dates, and honest about it when they cannot.\n\n## Company enrichment\n\nTurn on **Include current company details** to add each person's employer: website, domain, industry,\ncompany size, employee count, company type, headquarters, offices, founding year, description, logo.\n\nEach unique company is looked up **once per run and charged once**. 100 people at one company is a\nsingle company charge, not 100. Failed or blocked lookups return the person without company data and\nare never charged.\n\n## Email enrichment\n\nTurn on **Find business email** (automatic with *Full + contact*). Emails come from two places, and\nthe output records which:\n\n- `contact.source: \"linkedin_public\"` — the address was published in public LinkedIn text. **Free,\n  never charged.**\n- `contact.source: \"enrichment\"` — the address came from a third-party enrichment provider. **That\n  provider is not LinkedIn.**\n\n**Billing: you pay only for addresses you actually receive.** The `email-enrichment` event is charged\nonce per unique person for whom a verified address is returned. A lookup that finds nothing, returns\nonly a low-confidence result, or fails outright is **never charged**. Each unique person is looked up\nat most once per run; duplicates and skipped people are never looked up. Free addresses stay free.\n\n> **Paid email lookup requires a paid Apify plan.** On the Apify Free plan the third-party lookup is\n> skipped, nothing is charged for it, and no `email-enrichment` events fire. Emails **published** in\n> public LinkedIn text are still extracted on every plan and are always free. The run log says so\n> plainly when it applies.\n\n**Only confirmed addresses are returned.** `emailStatus: \"verified\"` means the provider tested the\nmailbox and it exists. Anything the provider could not confirm is **discarded rather than handed to\nyou**, and never charged.\n\n**Why that matters more than it sounds.** Many large employers run mail gateways that accept *every*\nrecipient — that is how enterprises defeat address-harvesting. At such a domain a finder will return a\npattern-derived address for **any name at all**. We tested this: asked for a person who does not exist\nat one such employer, the provider returned a plausible address for them, flagged internally as\n*generated*. An address obtained that way is evidence of nothing — not that the mailbox exists, not\neven that the person works there.\n\nSo those are dropped. **The consequence is real and intended:** expect an empty email column for a\nsubstantial share of people at large enterprises. A blank costs you nothing; a fabricated address costs\nyou your sending reputation.\n\n**People with no employer domain cannot be looked up at all.** A finder is given a name and a company\ndomain; without a domain there is nothing to search. The run log reports how many people were in that\nposition, so an empty column can be told apart from a vendor that found nothing.\n\n`emailStatus` only ever uses the provider's own vocabulary — `verified`, `valid`, `risky`,\n`unverified`, `not_found`. A status is never invented. Addresses found free in LinkedIn text are\n`unverified`: we can see them but have not tested them.\n\n## Deduplication and incremental runs\n\n- **Remove duplicate people** (default on) returns each person once, matched on LinkedIn profile ID,\n  then username, then profile URL. So the same person supplied as a URL, a username, and an ID counts\n  once and is charged once.\n- **Skip lists** — paste profile IDs, usernames, or profile URLs you already have. Matched people are\n  not extracted, not enriched, not saved, and **never charged**. Entries are cross-matched against all\n  three lists, so a URL pasted into the ID field still works.\n\n## Example output\n\nOne record per person. **This example reflects the real null pattern** — a typical row recovered\nthrough the paid fallback, which is ~85% of output. Fields LinkedIn does not publish are shown as\nthey actually arrive, rather than filled in with plausible-looking values.\n\n```json\n{\n  \"profileId\": null,\n  \"publicIdentifier\": \"jane-smith\",\n  \"linkedinUrl\": \"https://www.linkedin.com/in/jane-smith/\",\n  \"linkedinNumericId\": \"801048736\",\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Smith\",\n  \"fullName\": \"Jane Smith\",\n  \"headline\": null,\n  \"about\": \"Clinical pharmacist focused on medication safety…\",\n  \"location\": {\n    \"text\": \"New York City Metropolitan Area\",\n    \"city\": \"New York City Metropolitan Area\",\n    \"region\": null,\n    \"country\": null,\n    \"countryCode\": \"US\"\n  },\n  \"profileImageUrl\": \"https://media.licdn.com/dms/image/…\",\n  \"hasDefaultProfileImage\": false,\n  \"bannerImageUrl\": \"https://static.licdn.com/aero-v1/sc/h/…\",\n  \"openToWork\": null,\n  \"hiring\": null,\n  \"isInfluencer\": false,\n  \"isMemorialized\": false,\n  \"currentPosition\": {\n    \"title\": null,\n    \"companyName\": \"Example Health\",\n    \"companyId\": \"example-health\",\n    \"companyLinkedInUrl\": \"https://www.linkedin.com/company/example-health\",\n    \"startDate\": null,\n    \"endDate\": null,\n    \"location\": null,\n    \"description\": null\n  },\n  \"experience\": [\n    {\n      \"title\": null,\n      \"companyName\": \"Example Health\",\n      \"companyLinkedInUrl\": \"https://www.linkedin.com/company/example-health\",\n      \"companyId\": \"example-health\",\n      \"startDate\": null,\n      \"endDate\": null,\n      \"isCurrent\": true,\n      \"durationMonths\": null,\n      \"location\": null,\n      \"description\": null\n    }\n  ],\n  \"affiliations\": [\"Academy of Managed Care Pharmacy (AMCP)\"],\n  \"education\": [\n    {\n      \"schoolName\": \"Long Island University\",\n      \"schoolLinkedInUrl\": \"https://www.linkedin.com/company/long-island-university\",\n      \"degree\": \"Doctor of Pharmacy (PharmD)\",\n      \"fieldOfStudy\": null,\n      \"startDate\": \"2014\",\n      \"endDate\": \"2018\"\n    }\n  ],\n  \"skills\": [],\n  \"certifications\": [\"Basic Life Support (BLS)\", \"Immunization Delivery\"],\n  \"languages\": [\"English\", \"Spanish\"],\n  \"projects\": [],\n  \"publications\": [],\n  \"volunteerExperience\": [\"Community Health Fair — Volunteer Pharmacist\"],\n  \"honorsAwards\": [],\n  \"courses\": [],\n  \"recommendationsCount\": null,\n  \"followerCount\": 410,\n  \"connectionsCount\": 411,\n  \"websites\": [],\n  \"peopleAlsoViewed\": [\n    {\n      \"linkedinUrl\": \"https://www.linkedin.com/in/other-person/\",\n      \"name\": \"Other Person, PharmD\",\n      \"headline\": \"Weill Cornell Medicine\",\n      \"locationText\": \"662 followers Queens, NY\"\n    }\n  ],\n  \"totalYearsExperience\": null,\n  \"yearsAtCurrentCompany\": null,\n  \"experienceCount\": 1,\n  \"educationCount\": 1,\n  \"skillsCount\": 0,\n  \"currentCompany\": {\n    \"name\": \"Example Health\",\n    \"website\": \"https://www.examplehealth.com\",\n    \"domain\": \"examplehealth.com\",\n    \"industry\": \"Hospitals and Health Care\",\n    \"employeeCount\": 12400,\n    \"companySize\": \"10,001+ employees\",\n    \"headquarters\": \"New York, New York\",\n    \"foundedYear\": 1998\n  },\n  \"contact\": {\n    \"email\": \"jane.smith@examplehealth.com\",\n    \"emailStatus\": \"verified\",\n    \"emailType\": \"business\",\n    \"source\": \"enrichment\",\n    \"provider\": \"hunter\"\n  },\n  \"source\": \"direct_profile\",\n  \"matchedSearches\": [\n    { \"source\": \"direct_profile\", \"input\": \"https://www.linkedin.com/in/jane-smith/\" }\n  ],\n  \"searchPlanId\": null,\n  \"detailStatus\": \"ok\",\n  \"detailSource\": \"tikhub\",\n  \"scrapedAt\": \"2026-08-19T02:09:11.759Z\"\n}\n```\n\n**Every key is always present.** A field LinkedIn did not publish is `null` or `[]`, never omitted —\nso CSV columns stay aligned and you never have to guard an access. `currentCompany` and `contact`\nare `null` unless you enabled those options.\n\n**`matchedSearches` records which of your inputs produced the row**, which matters when the same\nperson was supplied twice in different forms — a URL and a bare username collapse to one row, and\nthis shows both inputs that led to it. `searchPlanId` is always `null`: it belonged to a search\nfeature this Actor does not have.\n\n**`detailSource` tells you where each row came from**: `linkedin_public` for a row read straight off\nthe public page (richer, but LinkedIn serves only ~1 in 10), or `tikhub` for one recovered through\nthe paid fallback. `contact.source` does the same for the email — `linkedin_public` for an address\npublished in the profile text, `enrichment` for one found by the email finder, which is **not**\nLinkedIn.\n\n\n## Output views\n\n| View | Contents |\n|---|---|\n| **Overview** | Name, headline, location, current role, profile URL |\n| **Full profile** | Every field including work history, education, skills |\n| **Contacts** | Person, role, employer, discovered email and its source |\n| **Companies** | Enriched current-employer details |\n| **Search trace** | How each person was supplied, and how their detail was obtained |\n\n## Pricing\n\nPay per event, so you only pay for what you use:\n\n| Event | Charged |\n|---|---|\n| **Profile result** | per unique person returned with profile data. **Never charged for an empty row** — see below |\n| **Company enrichment** | per unique company per run — not per person |\n| **Email enrichment** | per unique person for whom a **confirmed** address is returned; misses, unconfirmable results and failed lookups are all free. **Requires a paid Apify plan.** See [Email enrichment](#email-enrichment) |\n\nOne result event per person. **Never charged:** duplicates, people removed by\nfilters, people on your skip lists, failed extractions, blocked company lookups, and emails found free\nin public LinkedIn text.\n\nCurrent rates are on the **Pricing** tab — this README deliberately quotes no figures, so it cannot go\nstale.\n\n### 🚦 Proxy policy\n\n**Apify Datacenter proxy is the default, and residential is not worth paying for here.**\n\nLinkedIn blocks a large share of public profile requests whatever the IP class. Measured on the same\n19 profiles: **17 of 19 blocked on datacenter, and 17 of 19 blocked on residential** — identical,\nplus residential added upstream proxy failures. A blocked profile still produces a result, recovered\nthrough a paid data provider, though that version carries fewer sections.\n\n**Bring your own provider** under **Custom proxy URLs** and the Actor uses it instead — that traffic\ngoes through your account:\n\n```\nhttp://user:pass@proxy.iproyal.com:12321\nhttp://user:pass@proxy.brightdata.com:22225\nhttp://user:pass@proxy.oxylabs.io:7777\n```\n\n**No proxy** is honoured exactly as set. Expect most profile pages to be blocked and recovered\nthrough the provider.\n\nEvery run reports what it used and what it cost in bandwidth — see the `Proxy:` and `Bandwidth:`\nlines in the log, and `economics.bandwidth` in the run's key-value store.\n\n## API usage\n\nWorks from the Apify Console, the Actor API, Tasks, Schedules, webhooks, and the Dataset API. Inputs\nare simple top-level fields with no deeply nested objects.\n\nEach run also writes a `RUN_SUMMARY` record to the key-value store: how many profiles were supplied,\nresolved, deduplicated and skipped, full vs partial rows saved, companies enriched, emails found,\nerrors, and how often the free profile page succeeded rather than needing the paid fallback.\n\n## Limitations\n\nStated plainly, because they are the things people most often expect and do not get:\n\n- **This actor cannot find people.** No searching by job title, location, keyword, industry, or\n  company. LinkedIn publishes no public people-search endpoint and no upstream provider we can use\n  offers one. You must supply the people you want.\n- **Email coverage is bounded by the employer domain.** A finder is given a name and a company\n  domain, so a person whose employer we could not resolve cannot be looked up at any price. On a\n  measured 19-person run, 17 carried a usable domain — that is the ceiling, before the finder's own\n  hit rate is applied. The run log reports both numbers.\n- **Email coverage is low at large enterprises, by design.** Their mail gateways accept every\n  recipient, so no provider can confirm a mailbox — and one asked anyway returns a pattern-derived\n  address for any name, including invented ones. We discard those rather than return them, so the\n  column is empty for those employers instead of full of guesses.\n  Lookups that do not happen are never charged.\n- **The logged-out page is the ceiling, and the ceiling is specific.** Measured on a real\n  52-profile run: **skills and publications are never returned** (0%), and the job **title**,\n  **headline** and **years of experience / tenure** arrive on only 6% of rows (employment usually\n  arrives without dates, so there is nothing to compute tenure from). Languages come through 50% of\n  the time and certifications 35%, so those are occasional rather than reliable — the full table is in\n  [What the data looks like in practice](#what-the-data-looks-like-in-practice). Missing sections\n  come back empty rather than guessed, and no proxy setting or configuration changes what LinkedIn\n  publishes.\n- **Job titles are frequently redacted** to asterisks for logged-out visitors, so\n  `currentPosition.title` is often `null` while `currentPosition.companyName` is populated. The\n  actor never emits a redacted value as though it were data.\n- **Experience and tenure filters need dated work history** and cannot be evaluated for most\n  profiles. They keep those people rather than dropping them, and the run log reports exactly\n  how often that happened.\n- **Recommendations are counted, not reproduced.** `recommendationsCount` only — the text is a\n  third-party personal statement.\n- **Courses are not mapped.** The public page exposes LinkedIn Learning courses *authored by* the\n  person, which is not the same as coursework studied, so it is deliberately left empty.\n- **`openToWork` / `hiring` are `null`** when no profile page was read. They are only `false` once a\n  page has actually been parsed — the actor does not assert a fact it did not observe.\n\n## FAQ\n\n**Do I need a LinkedIn account, cookies, or `li_at`?**\nNo. The actor only reads pages that are public, and never asks for credentials.\n\n**Can I search for people by job title or location?**\nNo — see Limitations. Supply the profiles you want and use the filters to narrow them.\n\n**I supplied the same person as a URL and a username. Am I charged twice?**\nNo. They are recognised as one person, returned once, and charged once.\n\n**Will I be charged for people I already have?**\nNo. Put their IDs, usernames, or URLs in the skip lists and they are never extracted, enriched, saved,\nor charged.\n\n**Some rows came back almost empty. Was I charged for them?**\nNo. A row carrying only the URL you supplied — no name, no employer, no email — is returned so you can\nsee who LinkedIn withheld, and no event is charged for it. Those rows are counted in the run log.\n\n**Why is a person's email empty when Find business email is on?**\nOne of four reasons, and none of them is charged: they publish no address and we could not resolve\ntheir employer's domain, so there was nothing to search on; the finder searched and found nothing; it\nfound an address at a domain that *could* be verified but could not confirm it, so we discarded it; or\nthe lookup failed. The run log breaks down which.\n\n**Why is the email column empty for most people at a big company?**\nBecause their employer's mail gateway accepts every recipient, so no provider can confirm whether a\nmailbox exists. A finder asked about such a domain returns a pattern-derived address for any name — we\nverified that it does so even for a person who does not exist — so we discard those instead of\nreturning them. You are never charged for them.\n\n**Why are skills / certifications empty, and would a proxy fix it?**\nNo. LinkedIn does not publish those sections to logged-out visitors for most people, so they are\nabsent from pages that load perfectly. This is a limit of scraping without credentials, not a\nblocking or proxy problem.\n\n**I set a minimum years of experience and got everyone back. Is the filter broken?**\nNo — it could not be evaluated. Most profiles do not publish dated work history, and the actor\nkeeps people it cannot assess rather than dropping them. The run log reports the count, and\n`RUN_SUMMARY.experienceFilterUnknownKept` records it.\n\n## Compliance and disclaimer\n\n- You are responsible for using this actor lawfully.\n- You must comply with applicable privacy and data-protection law, including any rules that apply to\n  processing personal data about identified individuals.\n- You must comply with the relevant platform terms and policies.\n- **Business email addresses may be obtained from a third-party enrichment provider and are not\n  necessarily extracted from the LinkedIn profile itself.** The `contact.source` field records which\n  applies for every row.\n- This actor reads only publicly accessible pages and data. It does not require, request, or use\n  LinkedIn credentials, cookies, or session tokens.\n",
    "actorDefinition": {
      "actorSpecification": 1,
      "name": "linkedin-people-search-profile-scraper",
      "title": "LinkedIn Profile Scraper",
      "description": "Scrape complete LinkedIn profiles from URLs, usernames, or profile IDs. Work history, education, skills, certifications, current-company details, and optional business-email enrichment. Profiles are deduplicated across inputs. No LinkedIn login or cookies required.",
      "version": "1.0",
      "buildTag": "latest",
      "environmentVariables": {
        "TIKHUB_API_KEY": "@tikhub_api_key_prod_linkedin",
        "EMAIL_VENDOR": "hunter",
        "HUNTER_API_KEY": "@hunter_api_key_prod",
        "ICYPEAS_API_KEY": "@icypeas_api_key_prod"
      },
      "meta": {
        "templateId": "js-crawlee-cheerio"
      },
      "input": {
        "title": "LinkedIn Profile Scraper",
        "description": "Scrape complete LinkedIn profiles from URLs, usernames, or profile IDs — work history, education, skills, current-company details and optional business-email enrichment. Optional filters narrow the profiles you supply. No LinkedIn login, cookies, li_at or session ID is ever required.",
        "type": "object",
        "schemaVersion": 1,
        "properties": {
          "profiles": {
            "title": "LinkedIn profiles",
            "type": "array",
            "description": "Paste any mix of full profile URLs (\"https://www.linkedin.com/in/example-user/\"), usernames / public identifiers (\"example-user\"), or LinkedIn internal profile IDs (\"ACoAAA8BYqEBCGLg...\"). The type of each entry is detected automatically, equivalent entries are merged, and malformed entries are reported and skipped without stopping the run.\n\nURLs and usernames are read from LinkedIn's public profile pages, which costs nothing upstream. Profile IDs are not resolvable as URLs, so those always use the metered provider.",
            "editor": "stringList",
            "default": [],
            "prefill": [
              "https://www.linkedin.com/in/williamhgates/"
            ],
            "sectionCaption": "LinkedIn Profiles",
            "sectionDescription": "The people to scrape. This is the only input that finds anyone — the actor looks up profiles you name, it does not search LinkedIn for new people. Filters below narrow this list."
          },
          "maxProfiles": {
            "title": "Maximum profiles",
            "type": "integer",
            "description": "Maximum number of unique people to save. A global run limit. Duplicates and profiles removed by filters do not count towards it and are never charged.",
            "minimum": 1,
            "maximum": 5000,
            "default": 100,
            "prefill": 100,
            "unit": "profiles"
          },
          "currentJobTitles": {
            "title": "Current job titles",
            "type": "array",
            "description": "Keep only people whose CURRENT title matches any of these, matched on title and headline.",
            "editor": "stringList",
            "default": [],
            "prefill": [],
            "sectionCaption": "Filters",
            "sectionDescription": "All optional. Each filter narrows the profiles you supplied above — none of them find new people. Filters that need work history or education require Profile data level \"Full profile\" or higher; the run warns you if you set one that cannot be evaluated."
          },
          "locations": {
            "title": "Locations",
            "type": "array",
            "description": "Keep only people in any of these locations, matched against the location text on their profile.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "pastJobTitles": {
            "title": "Past job titles",
            "type": "array",
            "description": "Keep only people who previously held any of these titles, matched against their work history. Requires Full profile data.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "pastCompanies": {
            "title": "Past companies",
            "type": "array",
            "description": "Keep only people who previously worked at any of these companies, matched against their work history. Requires Full profile data.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "industries": {
            "title": "Industries",
            "type": "array",
            "description": "Keep only people whose current employer is in any of these industries. Requires Include current company details.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "schools": {
            "title": "Schools",
            "type": "array",
            "description": "Match people who attended any of these schools. Accepts school or university names, LinkedIn school URLs, or school IDs. Where LinkedIn cannot filter on this directly it is applied after extraction and requires Full profile data.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "minimumYearsExperience": {
            "title": "Minimum years of experience",
            "type": "integer",
            "description": "Only keep people with at least this much total work experience. Computed from the work history, counting overlapping roles once. Requires Full profile data — in Basic mode there is no work history to measure, so this filter is ignored and the run warns you.",
            "minimum": 0,
            "maximum": 60,
            "unit": "years",
            "nullable": true
          },
          "maximumYearsExperience": {
            "title": "Maximum years of experience",
            "type": "integer",
            "description": "Only keep people with at most this much total work experience. Requires Full profile data; ignored in Basic mode.",
            "minimum": 0,
            "maximum": 60,
            "unit": "years",
            "nullable": true
          },
          "minimumYearsAtCurrentCompany": {
            "title": "Minimum years at current company",
            "type": "integer",
            "description": "Only keep people who have been at their current company for at least this long. Requires Full profile data, because the current role's start date is not in the search results; ignored in Basic mode.",
            "minimum": 0,
            "maximum": 60,
            "unit": "years",
            "nullable": true
          },
          "maximumYearsAtCurrentCompany": {
            "title": "Maximum years at current company",
            "type": "integer",
            "description": "Only keep people who have been at their current company for at most this long — useful for finding recent movers. Requires Full profile data; ignored in Basic mode.",
            "minimum": 0,
            "maximum": 60,
            "unit": "years",
            "nullable": true
          },
          "includeKeywords": {
            "title": "Must contain keywords",
            "type": "array",
            "description": "Only keep people whose profile text contains at least one of these. Matched case-insensitively against the headline, about section, current title, skills and work-history titles. A person whose profile could not be read at all is kept rather than dropped.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "excludeKeywords": {
            "title": "Exclude keywords",
            "type": "array",
            "description": "Drop people whose profile text contains any of these. Applied conservatively — matched on whole words, so excluding 'AI' does not drop someone called 'Said'.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "excludeCurrentCompanies": {
            "title": "Exclude current companies",
            "type": "array",
            "description": "Drop people who currently work at any of these companies. Accepts names, LinkedIn company URLs, or company IDs. Applied before any profile page is opened, so excluded people cost nothing.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "excludeJobTitles": {
            "title": "Exclude job titles",
            "type": "array",
            "description": "Drop people whose current title contains any of these words — for example 'Recruiter', 'Talent Acquisition'. Matched case-insensitively, and applied before any profile page is opened, so excluded people cost nothing.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "includeCurrentCompanyDetails": {
            "title": "Include current company details",
            "type": "boolean",
            "description": "Add the person's current employer: website, domain, industry, company size, employee count, company type, headquarters, offices, founding year, description and logo. Each unique company is looked up once per run and charged once — 100 people at one company is a single company charge, not 100. Failed or blocked lookups return the person without company data and are never charged.",
            "default": false,
            "prefill": false,
            "sectionCaption": "Enrichment",
            "sectionDescription": "Optional extras, each billed as its own event so you only pay for what you turn on."
          },
          "findEmail": {
            "title": "Find business email",
            "type": "boolean",
            "description": "Extract a business email address for each unique person. Two sources, and the output records which via contact.source.\n\nFREE: an address published in the public LinkedIn text this Actor already reads — their About section or their employer's description. Costs nothing and is never charged. Most people publish no address.\n\nPAID (requires a PAID Apify plan — on the Free plan this step is skipped entirely, nothing is charged, and the free extraction above still runs): the person's name and their employer's mail domain are sent to a third-party email finder. That provider is NOT LinkedIn, and such an address is labelled contact.source 'enrichment'. You are charged ONLY when the provider returns an address it has CONFIRMED to be deliverable.\n\nUNCONFIRMED ADDRESSES ARE DISCARDED, NOT RETURNED. Many large employers run mail gateways that accept every recipient, so no provider can confirm whether a mailbox exists there — and a provider asked about such a domain will return a pattern-derived address for any name at all, including one belonging to nobody. Those are dropped rather than handed to you, and never charged. The consequence is real: expect an empty email column for a substantial share of people at large enterprises, by design.\n\nEach unique person is looked up at most once per run. Duplicates, filtered profiles and skip-list matches are never looked up. People whose employer domain we could not resolve cannot be looked up at all — the run log reports how many.",
            "default": false,
            "prefill": false
          },
          "removeDuplicates": {
            "title": "Remove duplicate people",
            "type": "boolean",
            "description": "Return each person once, even when several searches find them or you also supply their profile directly. Matched on LinkedIn profile ID first, then public identifier, then profile URL. Every search that matched a person is preserved in matchedSearches, and merging never causes a second charge.",
            "default": true,
            "prefill": true,
            "sectionCaption": "Deduplication & Incremental",
            "sectionDescription": "Control repeats within this run, and skip people you already collected in earlier runs."
          },
          "skipProfileIds": {
            "title": "Skip profile IDs",
            "type": "array",
            "description": "LinkedIn internal profile IDs to skip — for example 'ACoAAA123...'. Matched people are not extracted, not enriched, not saved and never charged. Entries are cross-matched against all three skip lists, so a URL pasted here still works.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "skipPublicIdentifiers": {
            "title": "Skip usernames",
            "type": "array",
            "description": "Public identifiers / usernames to skip — for example 'john-smith'. Matched people are never extracted, enriched, saved or charged.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "skipProfileUrls": {
            "title": "Skip profile URLs",
            "type": "array",
            "description": "Full LinkedIn profile URLs to skip. Matched people are never extracted, enriched, saved or charged.",
            "editor": "stringList",
            "default": [],
            "prefill": []
          },
          "proxyConfiguration": {
            "title": "Proxy configuration",
            "type": "object",
            "description": "Apify Datacenter proxy is the default, and is what this Actor uses.\n\nRESIDENTIAL IS NOT USED, even if you select it. LinkedIn blocks a large share of public profile requests regardless of proxy type: measured across datacenter, residential, and residential with a regenerated browser fingerprint, the identical set of profiles was blocked every time — LinkedIn decides per profile, not per requester. Selecting residential would therefore cost bandwidth without changing a single result, so the Actor falls back to datacenter and says so in the log.\n\nBlocked profiles still produce a result, recovered through a paid data provider, though that version carries fewer sections.\n\nCUSTOM PROXY URLS: if you supply your own provider it is used in full, through your own proxy account. That is the supported way to control which IPs are used.\n\nNO PROXY: honoured as-is.",
            "editor": "proxy",
            "default": {
              "useApifyProxy": true
            },
            "prefill": {
              "useApifyProxy": true
            },
            "sectionCaption": "Proxy"
          }
        },
        "required": []
      },
      "output": {
        "actorOutputSchemaVersion": 1,
        "title": "LinkedIn People Search & Profile Scraper Output",
        "properties": {
          "overview": {
            "type": "string",
            "title": "Overview",
            "description": "One row per person: name, headline, location, current role, profile URL.",
            "template": "{{links.apiDefaultDatasetUrl}}/items?view=overview"
          },
          "full": {
            "type": "string",
            "title": "Full profile",
            "description": "Every field, including work history, education, skills and derived experience totals.",
            "template": "{{links.apiDefaultDatasetUrl}}/items?view=full"
          },
          "contacts": {
            "type": "string",
            "title": "Contacts",
            "description": "Lead view: person, role, employer and the discovered business email.",
            "template": "{{links.apiDefaultDatasetUrl}}/items?view=contacts"
          },
          "companies": {
            "type": "string",
            "title": "Companies",
            "description": "Enriched current-employer details for each person.",
            "template": "{{links.apiDefaultDatasetUrl}}/items?view=companies"
          },
          "searchTrace": {
            "type": "string",
            "title": "Search trace",
            "description": "Which search found each person and how their detail was obtained.",
            "template": "{{links.apiDefaultDatasetUrl}}/items?view=searchTrace"
          }
        }
      },
      "dockerfile": "../Dockerfile",
      "minMemoryMbytes": 512,
      "maxMemoryMbytes": 4096,
      "defaultMemoryMbytes": 1024,
      "storages": {
        "dataset": {
          "actorSpecification": 1,
          "views": {
            "overview": {
              "title": "Overview",
              "description": "One row per person: who they are, where they are, and what they do. Field order follows PRD §48.",
              "transformation": {
                "flatten": [
                  "location",
                  "currentPosition",
                  "currentCompany",
                  "contact"
                ],
                "fields": [
                  "fullName",
                  "headline",
                  "location.text",
                  "currentPosition.title",
                  "currentPosition.companyName",
                  "linkedinUrl",
                  "contact.email",
                  "currentCompany.industry",
                  "currentCompany.employeeCount",
                  "detailStatus",
                  "scrapedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "fullName": {
                    "label": "Name",
                    "format": "text"
                  },
                  "headline": {
                    "label": "Headline",
                    "format": "text"
                  },
                  "location.text": {
                    "label": "Location",
                    "format": "text"
                  },
                  "currentPosition.title": {
                    "label": "Title",
                    "format": "text"
                  },
                  "currentPosition.companyName": {
                    "label": "Company",
                    "format": "text"
                  },
                  "linkedinUrl": {
                    "label": "Profile",
                    "format": "link"
                  },
                  "contact.email": {
                    "label": "Email",
                    "format": "text"
                  },
                  "currentCompany.industry": {
                    "label": "Industry",
                    "format": "text"
                  },
                  "currentCompany.employeeCount": {
                    "label": "Employees",
                    "format": "number"
                  },
                  "detailStatus": {
                    "label": "Detail",
                    "format": "text"
                  },
                  "scrapedAt": {
                    "label": "Scraped",
                    "format": "date"
                  }
                }
              }
            },
            "full": {
              "title": "Full profile",
              "description": "Every field, including the work-history, education and skills sections. Sections are empty in Basic mode.",
              "transformation": {
                "flatten": [
                  "location",
                  "currentPosition",
                  "currentCompany",
                  "contact"
                ],
                "fields": [
                  "profileId",
                  "publicIdentifier",
                  "linkedinUrl",
                  "firstName",
                  "lastName",
                  "fullName",
                  "headline",
                  "about",
                  "location.text",
                  "location.city",
                  "location.region",
                  "location.country",
                  "location.countryCode",
                  "profileImageUrl",
                  "openToWork",
                  "hiring",
                  "followerCount",
                  "connectionsCount",
                  "websites",
                  "currentPosition.title",
                  "currentPosition.companyName",
                  "currentPosition.companyId",
                  "currentPosition.companyLinkedInUrl",
                  "currentPosition.startDate",
                  "currentPosition.endDate",
                  "currentPosition.location",
                  "currentPosition.description",
                  "experience",
                  "affiliations",
                  "education",
                  "skills",
                  "certifications",
                  "languages",
                  "projects",
                  "publications",
                  "volunteerExperience",
                  "honorsAwards",
                  "courses",
                  "recommendationsCount",
                  "totalYearsExperience",
                  "yearsAtCurrentCompany",
                  "experienceCount",
                  "educationCount",
                  "skillsCount",
                  "currentCompany.name",
                  "currentCompany.website",
                  "currentCompany.domain",
                  "currentCompany.industry",
                  "currentCompany.employeeCount",
                  "currentCompany.companySize",
                  "currentCompany.companyType",
                  "currentCompany.headquarters",
                  "currentCompany.foundedYear",
                  "currentCompany.description",
                  "currentCompany.logoUrl",
                  "contact.email",
                  "contact.emailStatus",
                  "contact.emailType",
                  "contact.source",
                  "source",
                  "searchPlanId",
                  "matchedSearches",
                  "detailStatus",
                  "detailSource",
                  "scrapedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "linkedinUrl": {
                    "label": "Profile",
                    "format": "link"
                  },
                  "profileImageUrl": {
                    "label": "Photo",
                    "format": "link"
                  },
                  "currentCompany.website": {
                    "label": "Company site",
                    "format": "link"
                  },
                  "openToWork": {
                    "label": "Open to work",
                    "format": "boolean"
                  },
                  "hiring": {
                    "label": "Hiring",
                    "format": "boolean"
                  },
                  "followerCount": {
                    "label": "Followers",
                    "format": "number"
                  },
                  "connectionsCount": {
                    "label": "Connections",
                    "format": "number"
                  },
                  "websites": {
                    "label": "Websites",
                    "format": "array"
                  },
                  "totalYearsExperience": {
                    "label": "Total years exp.",
                    "format": "number"
                  },
                  "yearsAtCurrentCompany": {
                    "label": "Years at company",
                    "format": "number"
                  },
                  "currentCompany.employeeCount": {
                    "label": "Employees",
                    "format": "number"
                  },
                  "currentCompany.foundedYear": {
                    "label": "Founded",
                    "format": "number"
                  },
                  "scrapedAt": {
                    "label": "Scraped",
                    "format": "date"
                  },
                  "linkedinNumericId": {
                    "label": "Numeric ID"
                  },
                  "bannerImageUrl": {
                    "label": "Banner"
                  },
                  "peopleAlsoViewed": {
                    "label": "People also viewed"
                  }
                }
              }
            },
            "contacts": {
              "title": "Contacts",
              "description": "Lead-generation view: name, role, employer and the discovered email. `contact.source` says whether the address was published on LinkedIn or found by the enrichment provider.",
              "transformation": {
                "flatten": [
                  "location",
                  "currentPosition",
                  "currentCompany",
                  "contact"
                ],
                "fields": [
                  "fullName",
                  "currentPosition.title",
                  "currentPosition.companyName",
                  "contact.email",
                  "contact.emailStatus",
                  "contact.emailType",
                  "contact.source",
                  "currentCompany.domain",
                  "location.text",
                  "linkedinUrl"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "fullName": {
                    "label": "Name",
                    "format": "text"
                  },
                  "currentPosition.title": {
                    "label": "Title",
                    "format": "text"
                  },
                  "currentPosition.companyName": {
                    "label": "Company",
                    "format": "text"
                  },
                  "contact.email": {
                    "label": "Email",
                    "format": "text"
                  },
                  "contact.emailStatus": {
                    "label": "Status",
                    "format": "text"
                  },
                  "contact.emailType": {
                    "label": "Type",
                    "format": "text"
                  },
                  "contact.source": {
                    "label": "Email source",
                    "format": "text"
                  },
                  "currentCompany.domain": {
                    "label": "Domain",
                    "format": "text"
                  },
                  "location.text": {
                    "label": "Location",
                    "format": "text"
                  },
                  "linkedinUrl": {
                    "label": "Profile",
                    "format": "link"
                  }
                }
              }
            },
            "companies": {
              "title": "Companies",
              "description": "The current employer of each person, as returned by company enrichment. Empty unless Include current company details is on.",
              "transformation": {
                "flatten": [
                  "currentCompany",
                  "currentPosition"
                ],
                "fields": [
                  "currentCompany.name",
                  "currentCompany.companyId",
                  "currentCompany.linkedinUrl",
                  "currentCompany.website",
                  "currentCompany.domain",
                  "currentCompany.industry",
                  "currentCompany.companySize",
                  "currentCompany.employeeCount",
                  "currentCompany.companyType",
                  "currentCompany.headquarters",
                  "currentCompany.foundedYear",
                  "fullName",
                  "currentPosition.title"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "currentCompany.name": {
                    "label": "Company",
                    "format": "text"
                  },
                  "currentCompany.linkedinUrl": {
                    "label": "LinkedIn",
                    "format": "link"
                  },
                  "currentCompany.website": {
                    "label": "Website",
                    "format": "link"
                  },
                  "currentCompany.domain": {
                    "label": "Domain",
                    "format": "text"
                  },
                  "currentCompany.industry": {
                    "label": "Industry",
                    "format": "text"
                  },
                  "currentCompany.companySize": {
                    "label": "Size",
                    "format": "text"
                  },
                  "currentCompany.employeeCount": {
                    "label": "Employees",
                    "format": "number"
                  },
                  "currentCompany.companyType": {
                    "label": "Type",
                    "format": "text"
                  },
                  "currentCompany.headquarters": {
                    "label": "HQ",
                    "format": "text"
                  },
                  "currentCompany.foundedYear": {
                    "label": "Founded",
                    "format": "number"
                  },
                  "fullName": {
                    "label": "Person",
                    "format": "text"
                  },
                  "currentPosition.title": {
                    "label": "Their title",
                    "format": "text"
                  }
                }
              }
            },
            "searchTrace": {
              "title": "Search trace",
              "description": "Which search found each person, and how the detail was obtained. A person matched by several searches appears once, with every match in matchedSearches.",
              "transformation": {
                "fields": [
                  "fullName",
                  "linkedinUrl",
                  "source",
                  "searchPlanId",
                  "matchedSearches",
                  "detailStatus",
                  "detailSource",
                  "scrapedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "fullName": {
                    "label": "Name",
                    "format": "text"
                  },
                  "linkedinUrl": {
                    "label": "Profile",
                    "format": "link"
                  },
                  "source": {
                    "label": "Found via",
                    "format": "text"
                  },
                  "searchPlanId": {
                    "label": "Search",
                    "format": "text"
                  },
                  "detailStatus": {
                    "label": "Detail",
                    "format": "text"
                  },
                  "detailSource": {
                    "label": "Detail source",
                    "format": "text"
                  },
                  "scrapedAt": {
                    "label": "Scraped",
                    "format": "date"
                  }
                }
              }
            }
          }
        }
      },
      "pricingModel": {
        "pricingType": "PAY_PER_EVENT",
        "pricingEvents": [
          {
            "name": "profile-result-full",
            "description": "Charged once per unique person returned with profile data. Every profile is requested at full detail, so this normally means the complete record — work history, education, current position and the rest. YOU ARE NEVER CHARGED FOR AN EMPTY ROW: if LinkedIn withholds someone's page and the paid fallback cannot recover it either, that person is still returned so you can see who was missed, but no event is charged for them. Duplicates, people removed by your filters, and skip-list matches are never charged."
          },
          {
            "name": "company-enrichment",
            "description": "Charged once per unique company successfully enriched in this run, not once per person. A company shared by 50 people is charged once. Failed or blocked enrichment is never charged."
          },
          {
            "name": "email-enrichment",
            "description": "Charged once per unique person for whom a VERIFIED business email is returned. You pay only for addresses the enrichment provider has confirmed to be deliverable — a lookup that finds nothing, or that returns an address the provider could not confirm, is never charged. Emails already visible in public LinkedIn text are found free and never charged. Each unique person is looked up at most once per run; duplicates, filtered profiles and skip-list matches are never looked up. REQUIRES A PAID APIFY PLAN: on the Apify Free plan the paid lookup is skipped entirely and this event never fires, though emails published in public LinkedIn text are still extracted free of charge. Also requires an email vendor to be configured."
          }
        ]
      },
      "readme": "# LinkedIn Profile Scraper\n\n![LinkedIn Profile Scraper](https://raw.githubusercontent.com/coregentdevspace/linkedin-people-search-profile-scraper-assets/main/thumbnail-linkedin-people-search-profile-scraper.png)\n\nTurn a list of LinkedIn profile URLs, usernames, or profile IDs into structured records —\nverified current employer, full company details, location, and about text.\n\n**No LinkedIn login, cookies, `li_at`, or session ID is ever required.**\n\n> **What you reliably get, and what you do not.** This actor reads LinkedIn's *public,*\n> *logged-out* pages. Name, employer, location, education and photo come back on virtually every\n> profile; certifications and languages on most. But LinkedIn withholds job **titles**, **skills**\n> and employment **dates** from logged-out visitors entirely, so those arrive empty rather than\n> invented — and no proxy or setting changes that. Every figure is measured, not estimated:\n> [What the data looks like in practice](#what-the-data-looks-like-in-practice).\n\n## What does this actor do?\n\nYou give it the people you want. It returns one clean, normalized record each — deduplicated,\nwith the current employer resolved to a real LinkedIn company and optionally enriched with that\ncompany's industry, size, website and headquarters.\n\nPaste any mix of full profile URLs, usernames, or LinkedIn internal profile IDs. The actor detects\neach form automatically, merges equivalent entries, deduplicates people supplied more than once, and\nreturns a consistent record for every one — with optional filters to narrow a large input list.\n\nIt reads LinkedIn's **public** profile pages. You never supply a username, password, cookie, `li_at`\ntoken, or session ID.\n\n> **This actor does not search LinkedIn.** It looks up people you name. There is no public LinkedIn\n> people-search endpoint, so finding new people by job title, location, or keyword is not something\n> this actor can do — see [Limitations](#limitations).\n\n## Main use cases\n\n- **Verify and enrich a contact list** — turn profile URLs into confirmed employer, location and\n  company data. This is what the actor does best.\n- **Account mapping** — resolve a list of people to the companies they work at, with industry,\n  size and domain attached.\n- **CRM hygiene** — refresh employers and locations for existing contacts, and spot movers.\n- **Lead enrichment** — add the company domain you need for downstream email tooling.\n- **Automation** — call it from the Apify API, a Task, a Schedule, or a webhook.\n\n## Quick start\n\n```json\n{\n  \"profiles\": [\n    \"https://www.linkedin.com/in/example-user/\",\n    \"another-user\",\n    \"ACoAAA8BYqEBCGLg...\"\n  ],\n  \"profileDataLevel\": \"full\"\n}\n```\n\n## What you get\n\nEvery profile is returned at **full detail** — there is no tier to choose.\n\n| Field | Returned |\n|---|---|\n| Name, location, profile URL | ✅ always |\n| Current employer — name, LinkedIn URL, slug | ✅ always |\n| Profile photo | ✅ always |\n| About / summary | usually — 65% |\n| Education | usually — 77% |\n| Work history (employer; **dates are not published**) | usually — 87% |\n| Languages | often — 50% |\n| Certifications | sometimes — 35% |\n| Volunteering, honours | sometimes — ~20% |\n| Job title, skills, tenure totals | **not published by LinkedIn** — see below |\n| Current company details | opt-in — **Include current company details** |\n| Business email | opt-in — **Find business email** |\n| LinkedIn numeric member ID | when the paid fallback ran |\n| Banner image + default-photo flag | when the paid fallback ran |\n| Influencer / memorialized flags | when the paid fallback ran |\n| \"People also viewed\" — 10 related profiles | when the paid fallback ran |\n\nThose percentages are from a real 52-profile run, not estimates — the full table is in\n[What the data looks like in practice](#what-the-data-looks-like-in-practice). Sections LinkedIn\nwithholds come back empty rather than guessed, and a filter that depends on missing data tells you so\nin the run log instead of silently passing everyone.\n\n**Two of those need a word of explanation.** `hasDefaultProfileImage: true` means\n`profileImageUrl` is LinkedIn's grey placeholder rather than a real photo — the placeholder is a\nperfectly valid image URL, so without the flag you cannot tell them apart. And `linkedinNumericId` is\nLinkedIn's internal member id, stable even when someone changes their profile URL; it is not the\n`ACoAAA…` URN, which this data source does not expose at all.\n\n`peopleAlsoViewed` lists 10 profiles LinkedIn considers related. It is returned as data and is\n**never crawled** — this Actor only fetches people you named, so those entries are a starting point\nfor you rather than rows you will be charged for.\n\nThose five fields come from the paid fallback, which runs when LinkedIn blocks the public page — so in\npractice most rows have them and the few served from the public page do not. They are always present\nas keys, empty where unavailable.\n\n**Why there is no Basic tier.** There used to be, and it was removed because it saved nobody\nanything. Every person you supply has their profile page opened regardless — the basic fields come\nfrom that page — so a Basic row did exactly the same work and then had fields stripped before being\nreturned. It cost us the same to produce and cost you less, which made it the worst possible deal for\nboth sides. Full detail for everyone is simpler and honest.\n\n**One result event per person, and none at all for an empty row.** If LinkedIn does not serve\nsomeone's profile page and the paid fallback cannot recover it either, that person is still returned —\nso you can see exactly who was missed rather than finding a short file — but **no event is charged for\nthem**. You pay for people you actually received data about.\n\n\n## What the data looks like in practice\n\n**Measured on a real 52-profile run**, not estimated — 52 people across 42 employers, 15 industries\nand 17 countries, deliberately spread so no single employer or profession skews the numbers. Every\nrow returned data; 42 came through the paid fallback and 10 from the public page directly.\n\n| Field | Populated | |\n|---|---|---|\n| Profile URL, `publicIdentifier` | **100%** | ✅ |\n| Name, photo, follower count, location | **98%** | ✅ |\n| Current employer (name + LinkedIn URL) | **98%** | ✅ |\n| Work history entries | **87%** | ✅ |\n| Connections count, numeric member ID, banner | **87%** | ✅ |\n| Education | **77%** | ✅ |\n| \"People also viewed\" (10 related profiles) | **77%** | ✅ |\n| About / summary | **65%** | |\n| Languages | **50%** | |\n| **Business email** *(opt-in)* | **45%** of people with a company domain — **40%** of all rows | |\n| Certifications | **35%** | |\n| Volunteering | 21% | |\n| Honours & awards | 19% | |\n| Other organisations / affiliations | 17% | |\n| Websites | 14% | |\n| Courses | 10% | |\n| Recommendations count | 8% | |\n| Headline, job **title**, years of experience / tenure | 6% | ⚠️ |\n| Projects | 4% | |\n| Skills, publications | **0%** | ⚠️ |\n| Open-to-work / hiring badges | **0%** | ⚠️ |\n| LinkedIn URN (`profileId`) | **0%** | ⚠️ |\n\n> **These figures replaced ones measured on 18 profiles who were all pharmacists.** Two moved\n> sharply: certifications 72% → 35% and affiliations 72% → 17%. Licensed professionals carry\n> credentials that a general population does not, so the old numbers flattered those two fields.\n> Everything else moved a few points at most.\n\n### The ⚠️ rows, explained\n\nThese are not bugs and no setting changes them. They are what LinkedIn does not publish to a\nlogged-out visitor, and what our data source does not carry.\n\n**Job titles are redacted.** LinkedIn returns them as asterisks on ordinary profiles. We treat a\nredacted value as absent rather than emitting `\"****\"` as though it were an answer — so\n`currentPosition.title` is `null` while `companyName` is populated. **Headline** is empty for the\nsame reason on most profiles.\n\n**Employment arrives without dates.** The employer is published; the start and end dates are not.\nSo `experience[]` is populated while `totalYearsExperience` and `yearsAtCurrentCompany` stay\n`null` — there is nothing to compute them from. `experienceCount: 1` beside\n`totalYearsExperience: null` is honest, not broken.\n\n**Skills and publications** are absent from the data source entirely. Verified against a captured\npayload rather than assumed.\n\n**Projects, recommendations, job title and tenure are rare rather than impossible** — 4%, 8%, 6% and\n6%. They come only from the minority of profiles LinkedIn serves in full to a logged-out visitor, so\nexpect them occasionally and never depend on them.\n\n**`profileId`** is the internal `ACoAAA…` URN, which our source does not expose. Use\n`publicIdentifier` (100%) or `linkedinNumericId` (87%) instead — the latter is stable even if\nsomeone changes their profile URL.\n\n**If your use case needs a job title, dated work history, or skills for every person**, this Actor\ncannot deliver that from public LinkedIn data, and neither can any proxy or setting.\n\n\n## Filters\n\nAll optional, and all applied **after** extraction to narrow the list you supplied. They never find\nnew people.\n\n| Filter | Matched against | Needs Full? |\n|---|---|---|\n| Current job titles | title + headline | no |\n| Locations | the profile's location text | no |\n| Must contain / exclude keywords | headline, about, title, skills, work history | beyond the headline, yes |\n| Exclude job titles / companies | title, headline, current employer | no |\n| Past job titles / companies | work history | **yes** |\n| Schools | education | **yes** |\n| Industries | the enriched company's industry | needs company enrichment |\n| Min/max years of experience | work history, overlapping roles counted once | **yes** |\n| Min/max years at current company | the current role's start date | **yes** |\n\nA filter that cannot be evaluated **keeps** the person rather than dropping them, so nobody is\nsilently discarded because a field failed to parse.\n\n**The run tells you when this happens.** If a filter could not be evaluated for some of your\nresults, the log says so with the count and the reason — for example *\"the Minimum years of\nexperience filter could not be evaluated for 9 of 10 saved profiles (90%), because LinkedIn does\nnot publish dated work history for them. Those people were KEPT rather than dropped, so this\nfilter excluded nobody among them.\"* The same counts appear in `RUN_SUMMARY`.\n\nThat matters most for the four experience and tenure filters, which need dated work history —\navailable for only a minority of profiles. They are genuinely useful when filtering a list of\npeople who do publish dates, and honest about it when they cannot.\n\n## Company enrichment\n\nTurn on **Include current company details** to add each person's employer: website, domain, industry,\ncompany size, employee count, company type, headquarters, offices, founding year, description, logo.\n\nEach unique company is looked up **once per run and charged once**. 100 people at one company is a\nsingle company charge, not 100. Failed or blocked lookups return the person without company data and\nare never charged.\n\n## Email enrichment\n\nTurn on **Find business email** (automatic with *Full + contact*). Emails come from two places, and\nthe output records which:\n\n- `contact.source: \"linkedin_public\"` — the address was published in public LinkedIn text. **Free,\n  never charged.**\n- `contact.source: \"enrichment\"` — the address came from a third-party enrichment provider. **That\n  provider is not LinkedIn.**\n\n**Billing: you pay only for addresses you actually receive.** The `email-enrichment` event is charged\nonce per unique person for whom a verified address is returned. A lookup that finds nothing, returns\nonly a low-confidence result, or fails outright is **never charged**. Each unique person is looked up\nat most once per run; duplicates and skipped people are never looked up. Free addresses stay free.\n\n> **Paid email lookup requires a paid Apify plan.** On the Apify Free plan the third-party lookup is\n> skipped, nothing is charged for it, and no `email-enrichment` events fire. Emails **published** in\n> public LinkedIn text are still extracted on every plan and are always free. The run log says so\n> plainly when it applies.\n\n**Only confirmed addresses are returned.** `emailStatus: \"verified\"` means the provider tested the\nmailbox and it exists. Anything the provider could not confirm is **discarded rather than handed to\nyou**, and never charged.\n\n**Why that matters more than it sounds.** Many large employers run mail gateways that accept *every*\nrecipient — that is how enterprises defeat address-harvesting. At such a domain a finder will return a\npattern-derived address for **any name at all**. We tested this: asked for a person who does not exist\nat one such employer, the provider returned a plausible address for them, flagged internally as\n*generated*. An address obtained that way is evidence of nothing — not that the mailbox exists, not\neven that the person works there.\n\nSo those are dropped. **The consequence is real and intended:** expect an empty email column for a\nsubstantial share of people at large enterprises. A blank costs you nothing; a fabricated address costs\nyou your sending reputation.\n\n**People with no employer domain cannot be looked up at all.** A finder is given a name and a company\ndomain; without a domain there is nothing to search. The run log reports how many people were in that\nposition, so an empty column can be told apart from a vendor that found nothing.\n\n`emailStatus` only ever uses the provider's own vocabulary — `verified`, `valid`, `risky`,\n`unverified`, `not_found`. A status is never invented. Addresses found free in LinkedIn text are\n`unverified`: we can see them but have not tested them.\n\n## Deduplication and incremental runs\n\n- **Remove duplicate people** (default on) returns each person once, matched on LinkedIn profile ID,\n  then username, then profile URL. So the same person supplied as a URL, a username, and an ID counts\n  once and is charged once.\n- **Skip lists** — paste profile IDs, usernames, or profile URLs you already have. Matched people are\n  not extracted, not enriched, not saved, and **never charged**. Entries are cross-matched against all\n  three lists, so a URL pasted into the ID field still works.\n\n## Example output\n\nOne record per person. **This example reflects the real null pattern** — a typical row recovered\nthrough the paid fallback, which is ~85% of output. Fields LinkedIn does not publish are shown as\nthey actually arrive, rather than filled in with plausible-looking values.\n\n```json\n{\n  \"profileId\": null,\n  \"publicIdentifier\": \"jane-smith\",\n  \"linkedinUrl\": \"https://www.linkedin.com/in/jane-smith/\",\n  \"linkedinNumericId\": \"801048736\",\n  \"firstName\": \"Jane\",\n  \"lastName\": \"Smith\",\n  \"fullName\": \"Jane Smith\",\n  \"headline\": null,\n  \"about\": \"Clinical pharmacist focused on medication safety…\",\n  \"location\": {\n    \"text\": \"New York City Metropolitan Area\",\n    \"city\": \"New York City Metropolitan Area\",\n    \"region\": null,\n    \"country\": null,\n    \"countryCode\": \"US\"\n  },\n  \"profileImageUrl\": \"https://media.licdn.com/dms/image/…\",\n  \"hasDefaultProfileImage\": false,\n  \"bannerImageUrl\": \"https://static.licdn.com/aero-v1/sc/h/…\",\n  \"openToWork\": null,\n  \"hiring\": null,\n  \"isInfluencer\": false,\n  \"isMemorialized\": false,\n  \"currentPosition\": {\n    \"title\": null,\n    \"companyName\": \"Example Health\",\n    \"companyId\": \"example-health\",\n    \"companyLinkedInUrl\": \"https://www.linkedin.com/company/example-health\",\n    \"startDate\": null,\n    \"endDate\": null,\n    \"location\": null,\n    \"description\": null\n  },\n  \"experience\": [\n    {\n      \"title\": null,\n      \"companyName\": \"Example Health\",\n      \"companyLinkedInUrl\": \"https://www.linkedin.com/company/example-health\",\n      \"companyId\": \"example-health\",\n      \"startDate\": null,\n      \"endDate\": null,\n      \"isCurrent\": true,\n      \"durationMonths\": null,\n      \"location\": null,\n      \"description\": null\n    }\n  ],\n  \"affiliations\": [\"Academy of Managed Care Pharmacy (AMCP)\"],\n  \"education\": [\n    {\n      \"schoolName\": \"Long Island University\",\n      \"schoolLinkedInUrl\": \"https://www.linkedin.com/company/long-island-university\",\n      \"degree\": \"Doctor of Pharmacy (PharmD)\",\n      \"fieldOfStudy\": null,\n      \"startDate\": \"2014\",\n      \"endDate\": \"2018\"\n    }\n  ],\n  \"skills\": [],\n  \"certifications\": [\"Basic Life Support (BLS)\", \"Immunization Delivery\"],\n  \"languages\": [\"English\", \"Spanish\"],\n  \"projects\": [],\n  \"publications\": [],\n  \"volunteerExperience\": [\"Community Health Fair — Volunteer Pharmacist\"],\n  \"honorsAwards\": [],\n  \"courses\": [],\n  \"recommendationsCount\": null,\n  \"followerCount\": 410,\n  \"connectionsCount\": 411,\n  \"websites\": [],\n  \"peopleAlsoViewed\": [\n    {\n      \"linkedinUrl\": \"https://www.linkedin.com/in/other-person/\",\n      \"name\": \"Other Person, PharmD\",\n      \"headline\": \"Weill Cornell Medicine\",\n      \"locationText\": \"662 followers Queens, NY\"\n    }\n  ],\n  \"totalYearsExperience\": null,\n  \"yearsAtCurrentCompany\": null,\n  \"experienceCount\": 1,\n  \"educationCount\": 1,\n  \"skillsCount\": 0,\n  \"currentCompany\": {\n    \"name\": \"Example Health\",\n    \"website\": \"https://www.examplehealth.com\",\n    \"domain\": \"examplehealth.com\",\n    \"industry\": \"Hospitals and Health Care\",\n    \"employeeCount\": 12400,\n    \"companySize\": \"10,001+ employees\",\n    \"headquarters\": \"New York, New York\",\n    \"foundedYear\": 1998\n  },\n  \"contact\": {\n    \"email\": \"jane.smith@examplehealth.com\",\n    \"emailStatus\": \"verified\",\n    \"emailType\": \"business\",\n    \"source\": \"enrichment\",\n    \"provider\": \"hunter\"\n  },\n  \"source\": \"direct_profile\",\n  \"matchedSearches\": [\n    { \"source\": \"direct_profile\", \"input\": \"https://www.linkedin.com/in/jane-smith/\" }\n  ],\n  \"searchPlanId\": null,\n  \"detailStatus\": \"ok\",\n  \"detailSource\": \"tikhub\",\n  \"scrapedAt\": \"2026-08-19T02:09:11.759Z\"\n}\n```\n\n**Every key is always present.** A field LinkedIn did not publish is `null` or `[]`, never omitted —\nso CSV columns stay aligned and you never have to guard an access. `currentCompany` and `contact`\nare `null` unless you enabled those options.\n\n**`matchedSearches` records which of your inputs produced the row**, which matters when the same\nperson was supplied twice in different forms — a URL and a bare username collapse to one row, and\nthis shows both inputs that led to it. `searchPlanId` is always `null`: it belonged to a search\nfeature this Actor does not have.\n\n**`detailSource` tells you where each row came from**: `linkedin_public` for a row read straight off\nthe public page (richer, but LinkedIn serves only ~1 in 10), or `tikhub` for one recovered through\nthe paid fallback. `contact.source` does the same for the email — `linkedin_public` for an address\npublished in the profile text, `enrichment` for one found by the email finder, which is **not**\nLinkedIn.\n\n\n## Output views\n\n| View | Contents |\n|---|---|\n| **Overview** | Name, headline, location, current role, profile URL |\n| **Full profile** | Every field including work history, education, skills |\n| **Contacts** | Person, role, employer, discovered email and its source |\n| **Companies** | Enriched current-employer details |\n| **Search trace** | How each person was supplied, and how their detail was obtained |\n\n## Pricing\n\nPay per event, so you only pay for what you use:\n\n| Event | Charged |\n|---|---|\n| **Profile result** | per unique person returned with profile data. **Never charged for an empty row** — see below |\n| **Company enrichment** | per unique company per run — not per person |\n| **Email enrichment** | per unique person for whom a **confirmed** address is returned; misses, unconfirmable results and failed lookups are all free. **Requires a paid Apify plan.** See [Email enrichment](#email-enrichment) |\n\nOne result event per person. **Never charged:** duplicates, people removed by\nfilters, people on your skip lists, failed extractions, blocked company lookups, and emails found free\nin public LinkedIn text.\n\nCurrent rates are on the **Pricing** tab — this README deliberately quotes no figures, so it cannot go\nstale.\n\n### 🚦 Proxy policy\n\n**Apify Datacenter proxy is the default, and residential is not worth paying for here.**\n\nLinkedIn blocks a large share of public profile requests whatever the IP class. Measured on the same\n19 profiles: **17 of 19 blocked on datacenter, and 17 of 19 blocked on residential** — identical,\nplus residential added upstream proxy failures. A blocked profile still produces a result, recovered\nthrough a paid data provider, though that version carries fewer sections.\n\n**Bring your own provider** under **Custom proxy URLs** and the Actor uses it instead — that traffic\ngoes through your account:\n\n```\nhttp://user:pass@proxy.iproyal.com:12321\nhttp://user:pass@proxy.brightdata.com:22225\nhttp://user:pass@proxy.oxylabs.io:7777\n```\n\n**No proxy** is honoured exactly as set. Expect most profile pages to be blocked and recovered\nthrough the provider.\n\nEvery run reports what it used and what it cost in bandwidth — see the `Proxy:` and `Bandwidth:`\nlines in the log, and `economics.bandwidth` in the run's key-value store.\n\n## API usage\n\nWorks from the Apify Console, the Actor API, Tasks, Schedules, webhooks, and the Dataset API. Inputs\nare simple top-level fields with no deeply nested objects.\n\nEach run also writes a `RUN_SUMMARY` record to the key-value store: how many profiles were supplied,\nresolved, deduplicated and skipped, full vs partial rows saved, companies enriched, emails found,\nerrors, and how often the free profile page succeeded rather than needing the paid fallback.\n\n## Limitations\n\nStated plainly, because they are the things people most often expect and do not get:\n\n- **This actor cannot find people.** No searching by job title, location, keyword, industry, or\n  company. LinkedIn publishes no public people-search endpoint and no upstream provider we can use\n  offers one. You must supply the people you want.\n- **Email coverage is bounded by the employer domain.** A finder is given a name and a company\n  domain, so a person whose employer we could not resolve cannot be looked up at any price. On a\n  measured 19-person run, 17 carried a usable domain — that is the ceiling, before the finder's own\n  hit rate is applied. The run log reports both numbers.\n- **Email coverage is low at large enterprises, by design.** Their mail gateways accept every\n  recipient, so no provider can confirm a mailbox — and one asked anyway returns a pattern-derived\n  address for any name, including invented ones. We discard those rather than return them, so the\n  column is empty for those employers instead of full of guesses.\n  Lookups that do not happen are never charged.\n- **The logged-out page is the ceiling, and the ceiling is specific.** Measured on a real\n  52-profile run: **skills and publications are never returned** (0%), and the job **title**,\n  **headline** and **years of experience / tenure** arrive on only 6% of rows (employment usually\n  arrives without dates, so there is nothing to compute tenure from). Languages come through 50% of\n  the time and certifications 35%, so those are occasional rather than reliable — the full table is in\n  [What the data looks like in practice](#what-the-data-looks-like-in-practice). Missing sections\n  come back empty rather than guessed, and no proxy setting or configuration changes what LinkedIn\n  publishes.\n- **Job titles are frequently redacted** to asterisks for logged-out visitors, so\n  `currentPosition.title` is often `null` while `currentPosition.companyName` is populated. The\n  actor never emits a redacted value as though it were data.\n- **Experience and tenure filters need dated work history** and cannot be evaluated for most\n  profiles. They keep those people rather than dropping them, and the run log reports exactly\n  how often that happened.\n- **Recommendations are counted, not reproduced.** `recommendationsCount` only — the text is a\n  third-party personal statement.\n- **Courses are not mapped.** The public page exposes LinkedIn Learning courses *authored by* the\n  person, which is not the same as coursework studied, so it is deliberately left empty.\n- **`openToWork` / `hiring` are `null`** when no profile page was read. They are only `false` once a\n  page has actually been parsed — the actor does not assert a fact it did not observe.\n\n## FAQ\n\n**Do I need a LinkedIn account, cookies, or `li_at`?**\nNo. The actor only reads pages that are public, and never asks for credentials.\n\n**Can I search for people by job title or location?**\nNo — see Limitations. Supply the profiles you want and use the filters to narrow them.\n\n**I supplied the same person as a URL and a username. Am I charged twice?**\nNo. They are recognised as one person, returned once, and charged once.\n\n**Will I be charged for people I already have?**\nNo. Put their IDs, usernames, or URLs in the skip lists and they are never extracted, enriched, saved,\nor charged.\n\n**Some rows came back almost empty. Was I charged for them?**\nNo. A row carrying only the URL you supplied — no name, no employer, no email — is returned so you can\nsee who LinkedIn withheld, and no event is charged for it. Those rows are counted in the run log.\n\n**Why is a person's email empty when Find business email is on?**\nOne of four reasons, and none of them is charged: they publish no address and we could not resolve\ntheir employer's domain, so there was nothing to search on; the finder searched and found nothing; it\nfound an address at a domain that *could* be verified but could not confirm it, so we discarded it; or\nthe lookup failed. The run log breaks down which.\n\n**Why is the email column empty for most people at a big company?**\nBecause their employer's mail gateway accepts every recipient, so no provider can confirm whether a\nmailbox exists. A finder asked about such a domain returns a pattern-derived address for any name — we\nverified that it does so even for a person who does not exist — so we discard those instead of\nreturning them. You are never charged for them.\n\n**Why are skills / certifications empty, and would a proxy fix it?**\nNo. LinkedIn does not publish those sections to logged-out visitors for most people, so they are\nabsent from pages that load perfectly. This is a limit of scraping without credentials, not a\nblocking or proxy problem.\n\n**I set a minimum years of experience and got everyone back. Is the filter broken?**\nNo — it could not be evaluated. Most profiles do not publish dated work history, and the actor\nkeeps people it cannot assess rather than dropping them. The run log reports the count, and\n`RUN_SUMMARY.experienceFilterUnknownKept` records it.\n\n## Compliance and disclaimer\n\n- You are responsible for using this actor lawfully.\n- You must comply with applicable privacy and data-protection law, including any rules that apply to\n  processing personal data about identified individuals.\n- You must comply with the relevant platform terms and policies.\n- **Business email addresses may be obtained from a third-party enrichment provider and are not\n  necessarily extracted from the LinkedIn profile itself.** The `contact.source` field records which\n  applies for every row.\n- This actor reads only publicly accessible pages and data. It does not require, request, or use\n  LinkedIn credentials, cookies, or session tokens.\n"
    },
    "buildNumber": "1.0.9",
    "actVersion": {
      "sourceType": "SOURCE_FILES",
      "buildTag": "latest",
      "versionNumber": "1.0"
    },
    "usage": {
      "ACTOR_COMPUTE_UNITS": 0.006161111111111111
    }
  }
}