Companies & Profiles Linkedin scraper.
Get comprehensive profiles of individuals and companies based on your keywords and filters.
Unleash the power of data! 🌐🔍
Requests no longer claim to be two things at once. Every call carried
sec-fetch-user: ?1 and upgrade-insecure-requests: 1 — markers a browser only sends when
a person navigates to a page — alongside the headers of an authenticated background call.
No browser produces that combination. The HTTP client was adding them on its own; they are
now suppressed on API calls and kept only on the one request that really is a page load.
[10.49.1] — 2026-09-10
Fixed
Some profiles came back with "View {name}'s verifications" as their headline. The line the
page renders between the name and the headline was being taken for the headline itself whenever
it was written in an alphabet the filter did not cover — Greek, Finnish and Russian among them.
Measured on a live sample: 5 rows in 20 before, 0 in 20 after, with no headline lost.
[10.49.0] — 2026-09-10
Added
verified on profiles — whether the member carries LinkedIn's identity-verification badge.
Returned on every profile at no extra cost: it comes out of the response the Actor already
fetches, so runs are not slower and not more expensive.
[10.48.0] — 2026-09-10
Added
Courses and Publications are now returned, as two new profileFields options. LinkedIn has
always served these sections; the Actor knew how to ask for them and had nowhere to put the
answer, so they were never returned. Both appear as columns in the Profiles table.
[10.47.0] — 2026-09-10
Fixed
The run log misstated what it was about to do. Given four company URLs and a limit of 1 it
announced "up to 1 companies across 4 targets" and then returned four. The limit is a
per-search budget and has never applied to URLs you list directly; the log now says which of
the two applies, and what the run can return at most.
Changed
The log tells the story of the run instead of repeating itself. Each search names its term
and page and how many matches it found, progress is reported on a cadence rather than one line
per row, and the run closes on a count — including how many of your inputs matched nobody.
The Profiles and Companies tables show the whole record, not a tenth of it. Everything the
Actor returns is now a column: for a profile, the About text and the full experience, education,
certifications, skills, languages, honors, projects, organizations, volunteering and contact
sections alongside the identity and reach fields; for a company, the description, locations,
specialities, hashtags, phone, jobs link and page type alongside the size and reach fields. No
field sits behind the "all fields" toggle any more. Both tabs are labelled with an icon.
The Output tab links straight to profiles only, companies only, or a CSV export, instead of
a single undifferentiated dataset link.
[10.46.0] — 2026-09-10
Fixed
Runs are no longer routed halfway around the world. Traffic was leaving from whatever
country came up — eight consecutive draws landed in Italy, Canada, Brazil, the UK, South
Africa, Trinidad and Vietnam — while the site being read is served from the United States, so
a large share of requests timed out crossing the ocean twice. Measured over 4 072 requests, the
distant routing lost up to 44 % of a run's capacity; it now leaves from the United States by
default. Same input, same output, fewer timeouts and a faster run.
[10.45.2] — 2026-09-10
Fixed
Rows recovered from a results page that did not render its links came back with the profile's
identity but none of its detail — no experience, education, current position or summary. They
now carry the same detail as any other row: measured against the previous release on identical
keywords, experience went from 8 % to 99 % of rows, education 6 % to 90 %.
Those same rows were missing the input field, so callers could not line an output up with
the keyword that produced it. Every row carries it again.
Changed
The warning and status message shown when a run stops early now describe what the caller sees —
the run ended early and kept what it had — instead of naming internals.
[10.45.1] — 2026-09-10
Fixed
Internal reliability work; input and output unchanged.
[10.45.0] — 2026-09-10
Fixed
A keyword could be reported as matching nobody while it had a full page of matches. Some
results pages come back without their result links rendered; the run read those as empty,
returned a NO_RESULTS row for the keyword and stopped chasing it — which is why two runs on
the same keywords could return 340 rows and 8. Those pages are now read through the identifiers
they always carry, so the same keyword yields the same ten results per page either way.
[10.44.2] — 2026-09-10
Fixed
A keyword search could contribute one row that was not a search result. A results page
carries page furniture that reads like a match without being one, and it was being followed and
returned alongside the real results — roughly one row per search, about 4 % of a delivery. Those
rows are now excluded; genuine matches, including the top-ranked ones, are unaffected. Page
counts are unchanged: a full page still yields ten results.
[10.44.1] — 2026-09-10
Changed
Searches are requested the way a browser requests them. During an upstream incident the
previous form of the request returned an error page for several minutes while this one kept
working. Same results, same page size, same pagination, on both profiles and companies.
[10.44.0] — 2026-09-10
Changed
Runs are faster by default. The default parallelism was set conservatively long ago and
measurements showed it was holding runs back without protecting anything. Same input, same
output, more results per minute.
[10.43.0] — 2026-09-10
Fixed
A run no longer stalls on a network route that has stopped answering. One class of outage
was not recognised as such, so the run kept retrying the same dead route instead of switching
to a working one on the first signal. Output unchanged; affected runs finish faster and
deliver more.
Changed
Internal reliability work, off by default: input and output unchanged.
[10.42.0] — 2026-09-10
Fixed
A profile is no longer delivered with a section silently missing. When the response for one
part of a profile came back empty, that part was merged as nothing and the profile was still
emitted — so experience, education or skills could be absent from a row that reported success.
The empty response is now retried instead.
A run recovers cleanly when the site interrupts a connection mid-run. State that the site
asked the client to discard was being kept and replayed instead, which made the rest of the run
progressively less reliable. No input or output change.
[10.41.0] — 2026-09-09
Changed
Internal reliability work: the engine's working state is now held in memory for the lifetime of
the run in every mode. No input, output, or behavior change for callers — an interrupted run
still resumes where it left off.
[10.40.0] — 2026-09-09
Fixed
A run no longer stalls when an upstream network route stops working. It switches to a
working route automatically and remembers the choice, so later runs start on it directly.
[10.39.0] — 2026-09-09
Changed
Maintenance release. No input, output, or behavior change for callers.
[10.38.0] — 2026-09-09
Fixed
A run that ran out of capacity mid-way could fail outright, even though it had already
delivered results. It now finishes cleanly, keeps every row it collected, and reports how
many were delivered before it stopped.
[10.37.0] — 2026-09-09
Changed
Maintenance release. No input, output, or behavior change for callers.
[10.36.0] — 2026-09-09
Changed
Maintenance release. No input, output, or behavior change — build tooling and internal
documentation only.
Documentation
README brought up to date with everything shipped since 10.11: the company and
jobTitle search filters, the contact profile section, and the fields that were being
returned but not documented — input / status / reason, profileId, currentTitle,
currentCompany, careerStartYear, firstDegreeYear, contactInfo, industryId, and the
company link and ISO dates on each experience entry.
[10.35.1] — 2026-09-09
Fixed
The four filtering fields added in 10.35.0 shipped empty: they were derived after the row
had already been assembled. Building the row in one place fixes it and keeps it fixed.
[10.35.0] — 2026-09-09
Added
Four flat fields for filtering, so you don't have to walk the experience array on every
row: currentTitle and currentCompany (the role the profile still lists as ongoing),
careerStartYear (earliest year across the full history) and firstDegreeYear (earliest
year across education). All derived from data already in the record — no extra request, no
extra cost. A profile with no ongoing role gets no current fields rather than its most
recent past one presented as current.
The dataset table now shows input, status and reason on both profiles and companies,
plus the new current-role and career-start columns on profiles. input names what produced
each row, which is what ties an output back to the entry you sent.
Fixed
websiteUrl is always a usable URL. LinkedIn serves it exactly as the company typed it,
so it could arrive as www.example.com with no scheme — not fetchable, and not what the
field promises. A value that looks like a host now gets https://; anything else is left
exactly as received rather than dressed up as a URL.
[10.34.2] — 2026-09-09
Fixed
A repeated section no longer shifts every entry after it. LinkedIn's "Show all" response
renders its list twice. Where entries come in their own blocks that is harmless, but on the
fallback path the second copy was read as more entries — one school took the repeat's first
school as its DEGREE, and that degree then became a fourth school. The replay is now cut
before anything is parsed. A profile with several degrees at the same university is
untouched: only a tail that replays an earlier block is removed.
[10.34.1] — 2026-09-09
Fixed
profileId is now declared in the output schema, so it shows up in the dataset table
and preview like every other field. It has been in the data since 10.27.0 — only the
description was missing.
[10.34.0] — 2026-09-09
Changed
The skills link under a role is now recognised by its position, not by its wording.
It was matched against a list of translations, which would miss any language not on that
list and let the line reappear as a phantom job. LinkedIn lays an entry out as title,
employer, then metadata, so a further primary line after the metadata is never part of the
job — that is what identifies it now, in any language. Grouped blocks of consecutive roles
at one employer are told apart by carrying several dates and are left untouched.
[10.33.0] — 2026-09-09
Changed
Profile cards and detail pages are now read the same way. Since 10.32.0 the "Show all"
pages parsed each entry from its own block while the profile card still read one flat list
per section and guessed where entries ended — so the same profile could parse differently
depending on which path served it. Cards use the same boundary now. Entry counts are
unchanged on every captured profile; what changes is that an entry can no longer take a
field from its neighbour. Layouts that render no such blocks keep the previous behaviour.
[10.32.1] — 2026-09-09
Fixed
The "LinkedIn helped me get this job" badge is no longer returned as a job. It arrives
with a tail of its own title in the company field, which is how it is recognised without
depending on the wording; stray fragments carrying neither a date nor an employer go with
it. Rows keeping either field are untouched, so a self-employed entry with no company, or a
real role whose dates we read imperfectly, still comes through.
[10.32.0] — 2026-09-09
Fixed
Each experience and education entry is now read from its own container, so an entry can
no longer borrow the next one's employer. LinkedIn wraps every entry of a section in its own
block; we were reading one flat list of text and guessing where each entry ended. That guess
is what produced duplicated roles, a company name promoted to a job title, and rows with no
dates at all. Entry counts now match the profile exactly.
[10.31.0] — 2026-09-09
Fixed
A phantom "Skills:" entry no longer appears in experience — and no longer steals the next
role's employer. Found by auditing 22 profiles and 224 roles after the previous release:
the skills line LinkedIn renders under a role was read as a job title, and the following
role's title became its company name. It is a link to a skills overlay, not content, and is
now excluded in both the shapes LinkedIn renders it ("Skills: A, B" and "…and +5 skills").
A value split across inline markup is returned whole. Text carrying emphasis or a link
in the middle used to arrive as several separate values. Extraction now concatenates a text
run the way LinkedIn's own renderer does, so a label and its value stay one field.
[10.30.0] — 2026-09-09
Fixed
Experience and education are no longer cut off at the collapsed profile view. The
profile card renders only the first few entries; everything behind "Show all N" was never
fetched, and the entries missing were always the OLDEST — so someone who started in 1994
read as starting in 2015, with a payload that looked complete. Both sections now follow
that link. On the reported profile: 5 experiences → 16, 2 educations → 5, back to a 1998
first role. The education request is skipped when LinkedIn shows no "see all" link, so
short profiles cost nothing extra.
Multi-line descriptions are returned whole instead of being shredded into fake entries.
A description is one node whose children are its lines; reading them one by one kept only
the first line and re-read the rest as new experience entries — a line of prose becoming a
job title, a bullet becoming a company name. Descriptions now come back complete, with
their line breaks, and the invented entries are gone. Tell-tale of the old behaviour: no
returned description ever contained a newline.
A profile URL written with the wrong apostrophe now resolves. LinkedIn builds a handle
like …-d’avila-… with a typographic apostrophe; the ASCII one looks identical and gets
substituted by spreadsheets and CRMs, and LinkedIn 404s on it. A 404 on a slug containing
either apostrophe is now retried once with the other spelling. Only that substitution —
dropping the apostrophe entirely lands on a different member.
Vanity URLs with punctuation or symbols no longer truncate. a slug containing an apostrophe
was looked up as the part before it; registered marks, emoji and zero-width characters hit the
same wall. Worse than a miss: when the truncation happened to be someone else's real
handle, the run returned THE WRONG PERSON as FOUND. A slug is now bounded by URL
delimiters rather than by a list of allowed characters.
[10.29.0] — 2026-09-09
Fixed
company now filters by employer, so you can get the people who work somewhere. It was
being sent as a free-text keyword, which searches profile text rather than an employer —
filtering "engineer" by company Stripe returned a single profile, and not one at Stripe.
The field now resolves to the company and uses LinkedIn's real current-company filter; all
results come back at that company. It accepts a company URL, a slug or a name, and a name
that cannot be resolved falls back to the old keyword rather than losing your filter.
jobTitle is matched as an exact phrase. It was sent unquoted, so "Data Scientist" was
two loose words and matched far more than the title asked for.
[10.28.0] — 2026-09-09
Fixed
Companies now carry the industry label LinkedIn actually displays. The API we read still
serves the pre-rename taxonomy, so a company showed as "Computer Software" where the page
says "Software Development", and "Information Technology & Services" where it says "IT
Services and IT Consulting" — reported by users comparing our output against the profile in
front of them. The numeric industry id is the same on both sides, so the label is now
resolved through LinkedIn's published Industry Codes V2 table (434 industries). An industry
outside that table keeps the label the API served, and industryId still ships alongside
for anyone matching industries across systems.
[10.27.0] — 2026-09-09
Added
Location filtering now works on company search. It never did: the company vertical
ignores the geoUrn facet the people search uses — accepting it without error, which is
why adding a country quietly returned the same worldwide results. Its own facet is
companyHqGeo, read off LinkedIn's own filter rather than guessed. The location is
resolved the same way as for profiles and rides every page of results, so a long run
cannot drift back to worldwide halfway through.
Every profile row carries profileId, LinkedIn's stable member id (ACoAAA…). Unlike
the vanity name it survives the member editing their profile URL, and it is accepted back
as input, so it is the identifier to join on across systems. It was already being read on
every profile — it is what the follow-up requests are built from — but never reached the
output. Distinct from urn, the older numeric namespace LinkedIn rarely exposes.
[10.26.0] — 2026-09-09
Added
Numeric company URLs work: /company/1441 resolves to the real company. Asked for in three separate threads since April 2024. LinkedIn answers the id page with 200 and hops to the slug client-side, so there is no redirect to follow — the slug is read from the page, anchored on the tab links (/home/, /about/, /people/…) that only the current company renders. Verified live on the exact examples from those issues: 3653845 → Snowflake, 7208 → Crispin, 1441 → Google. Costs one extra request, and only for inputs that are ids.
When LinkedIn answers with its empty app frame instead of the rendered page — same URL, same request — the page is asked for again, up to three times. That frame name-drops a dozen other companies as often as the real one, so guessing from it is not an option; an id that never resolves ends up as an honest NOT_FOUND carrying its input.
[10.25.0] — 2026-09-09
Added
Two new search filters: company and jobTitle — both free text, both optional. They combine with keywords and location, which is the real AND across criteria that the "AND Filters" issue asked for, and they answer "give me the people at this company" without needing a company id. Verified live: keywords=engineer + company=Vinted + jobTitle=Data Scientist returns Staff and Senior Data Scientists at Vinted.
Blank values are ignored rather than sent as an empty filter, and a run without them behaves exactly as before (origin stays GLOBAL_SEARCH_HEADER).
[10.24.0] — 2026-09-08
Changed
Fewer lookups lost part-way through a long run. Internal reliability work on how the engine talks to LinkedIn. Input, output and pricing are unchanged.
[10.23.0] — 2026-09-08
Changed
Internal reliability work on the engine's outgoing requests, so a long run holds up from start to finish instead of degrading. Nothing about the input or the output changes.
[10.22.0] — 2026-09-08
Added
industryId on company records — LinkedIn's numeric industry id, alongside the existing label. The label itself comes from LinkedIn's older taxonomy (Computer Software where its UI now says Software Development), and that is not a field we picked wrong — LinkedIn serves the same entity under both names. The id is the one value that survives its renamings, so it is the safe thing to join on.
[10.21.1] — 2026-09-08
Fixed
input was lost when a profile changed identity mid-flight. Seeding an ACoA… id emits the profile under its real vanity name, so looking the entry up by the emitted name found nothing and the row came back with input: null. The caller's entry now travels with the request from the seed. Caught by a regression sweep on the deployed build, not by a test.
[10.21.0] — 2026-09-08
Added
Contact info as an opt-in profile section. Tick "Contact info" in "Profile data to scrape" and each profile gains a contactInfo object: the member's profile URL and their websites, each with the displayed label and the category they chose (Company, Blog, personal…). Outbound links are unwrapped from LinkedIn's safety/go redirector, so you get the real destination. One extra request per profile, hence opt-in. Asked for in 2024.
Only what the member made public is returned — most profiles expose no email address, and the field is simply absent then rather than guessed at.
[10.20.0] — 2026-09-08
Added
Profile IDs (ACoA…) are accepted as input, on their own, inside an /in/{id} URL, or wrapped in urn:li:fsd_profile:{id}. The actor used to refuse them with "Raw LinkedIn IDs are not supported"; that refusal was ours alone — LinkedIn's profile URL serves the full profile for an ID exactly as it does for a vanity name, so no id→slug resolution is involved. The output carries the profile's real vanity name, not the ID you sent (ACoAAACu9SEB… → joanmarcriera).
urn:li:member:{numeric} — LinkedIn's older ID namespace — is still refused, with a message that now says which ID form does work. That URN is not a profile-URL path segment.
[10.19.0] — 2026-09-08
Added
/school/ and /showcase/ URLs are accepted on get-companies. LinkedIn serves both from the very same company endpoint — only our input marker was turning them away, before we even tried. Asked for in 2024 and promised then. Verified live: Smith College and Charter College (schools), Microsoft Azure, Generali Global Assistance and InnerOrbit (showcase pages) all return full company records.
[10.18.0] — 2026-09-08
Added
Every output row names the input that produced it, in a new input field — the search term for a search result, the entry you actually typed for a direct URL (query string and all, not the slug we derived from it). Asked for in three separate threads since April 2024. Outputs and inputs now line up without relying on ordering or parsing the run log. Two encodings of the same profile collapse to one target, and the first entry wins.
[10.17.2] — 2026-09-08
Fixed
The engine's requests are aligned with what a real browser sends, from a live capture of LinkedIn's own traffic. Verified live: profiles, experience and company links all still come back.
[10.17.1] — 2026-09-08
Fixed
A path that has stopped answering is now abandoned the first time it says so, instead of being retried until the run runs out of attempts. Measured over five weeks, the previous policy never once took effect — which is why a run that started badly stayed bad for its whole length. Fewer wasted attempts, more results per run.
[10.17.0] — 2026-09-08
Added
companyId and companyUrl on every experience entry — the most requested field on the actor, asked for across four separate issue threads since January 2024. Feed either straight into the company scraper to join profiles to companies. No extra request: LinkedIn already ships the link with the experience card.
The mapping follows LinkedIn's own grouping — one company link per consecutive run of roles at the same employer, so several roles at one company all carry it. An employer with no LinkedIn page yields an entry with no id rather than shifting the mapping onto the wrong company; if the counts ever disagree, no link is attached at all rather than risk placing someone at an employer they never worked for.
[10.16.3] — 2026-09-08
Fixed
The location filter works again. It was not the geo lookup itself: the filter was resolved once, at startup, on a single attempt — so a run whose first attempt failed lost the filter for its whole length and quietly searched worldwide (searches, which retry, were unaffected). Each attempt now starts clean, and the budget is 4. Verified live: Cyprus resolved to geo id 106774002 on the second attempt and the results came back from Limassol and Nicosia.
[10.16.2] — 2026-09-08
Fixed
A location filter that cannot be resolved no longer becomes a silent worldwide search. When the location lookup failed, the run dropped the filter, logged a warning and searched the whole world — the caller received unfiltered data that looked filtered, with nothing in the output to say so (seen on a production run: software engineer + Cyprus). The lookup is now retried once, and if it still fails the affected searches are skipped and each keyword gets a row { input, status: "NOT_FOUND", reason: "LOCATION_UNRESOLVED" }. Direct profile URLs are unaffected. New LOCATION_UNRESOLVED value in the dataset schema's reason enum.
The run now reports every location it could not resolve, instead of only warning about the first.
[10.16.1] — 2026-09-08
Fixed
A profile record is never reported as FOUND when it carries no data. A vanity URL that no longer resolves (a renamed profile) returns a page that is neither LinkedIn's 404 nor a profile. Once the retries gave up, that page was emitted with status: "FOUND" and nothing but the input in it — a false positive the caller could not tell from a real result. Such a record is now catalogued NOT_FOUND with the new reason NO_DATA. Identity fields or any non-empty section count as data, so a profile carrying only sections (no identity block) is still FOUND.
New NO_DATA value in the dataset schema's reason enum, with the enum's description rewritten to cover all four reasons.
[10.16.0] — 2026-09-08
Added
startDateISO / endDateISO on every experience entry. Machine-readable siblings of the existing dates: "Jan 2013" → "2013-01", a bare "2013" stays "2013", and an open-ended "Present" yields no ISO value. startDate / endDate keep LinkedIn's original strings unchanged — nothing that parses them today breaks.
Every input now leaves a row in the dataset. A keyword that matches nobody used to produce nothing at all, so callers could not line inputs up with outputs. Such a search now emits one record { input, status: "NOT_FOUND", reason: "NO_RESULTS" } (people and company searches alike, page 1 only). It is not counted as a delivered result and consumes no daily quota. New input field and new NO_RESULTS value in the reason enum of the dataset schema.
Changed
Brought the engine back in step with LinkedIn's current client — the pinned version was ~1000 builds stale. Verified against a live run (7 requests, 0 failures).
[10.15.2] — 2026-09-08
Fixed
Profile / company URLs with a non-ASCII vanity slug are no longer truncated. Slugs were bounded by an ASCII-only character class, so the lookup stopped at the first accented character — a slug was cut at its first accented letter, sometimes down to two characters. Percent-encoding the URL client-side hit the same wall (% ended the class too), so there was no caller-side workaround. Most truncations 404'd as NOT_FOUND; some resolved to a real but unrelated member, returned as a FOUND match for the requested person. Slugs are now matched as Unicode and percent-decoded once (the percent-encoded and the accented form of the same slug are one target), then re-encoded exactly once for the request. Affects ~12–20% of profiles in datasets with Portuguese / Spanish / French names.
Same fix on the keyword-search path: accented /in/{vanity} and /company/{universalName} slugs found in a page of search results were silently dropped instead of being kept. Verified on a live engenheiro civil search: 6 members extracted before, 8 after, none lost.
[10.15.1] — 2026-07-30
Changed
Internal pacing tuning. No change to input or output.
[10.15.0] — 2026-07-30
Changed
profileFields default is now ["about"] (cheapest section: 1 card, 0 pagers), not “scrape everything”. Empty / omitted / unknown-only input no longer fetches all cards+pagers — pick the sections you need, or select every option for a full scrape. Schema default/prefill updated so the Apify UI starts with About.
[10.14.3] — 2026-07-30
Changed
Default request concurrency settled at a middle ground after a higher setting proved too aggressive on live runs. Use profileFields when you want a run to go faster.
[10.14.2] — 2026-07-30
Changed
A profile's sections are fetched side by side again instead of strictly one after another, so each profile completes faster.
[10.14.1] — 2026-06-23
Changed
Routine maintenance to keep the engine in step with LinkedIn's current client, before the pinned version goes stale. Both versions served data in testing — this is upkeep, not a fix.
[10.14.0] — 2026-06-23
Fixed
A run no longer burns its attempts on a path that has stopped answering (v10.13 regression). LinkedIn bounces such a request into a redirect loop, which v10.13 could only see as an opaque Too many redirects — so it kept retrying the same dead path indefinitely and the success rate collapsed while the run still looked healthy. The loop is now recognised for what it is and the run moves on immediately, instead of spending ten redirects and eight retries on it.
Success is only credited on a real 2xx. Previously any status below 400 — a bounce included — counted as one.
[10.13.0] — 2026-06-15
Fixed
Transient network trouble no longer costs a run its momentum. Connection resets, rate limits, timeouts and empty responses were being handled as though they were permanent failures; they are now told apart and simply retried, which removes the main source of churn on a long run.
Changed
Routine maintenance to keep the engine in step with LinkedIn's current client, now held in a single place for easier upkeep.
Added
Optional tuning knobs for request concurrency and pacing, so a run's aggressiveness can be adjusted without a code change.
[10.12.0] — 2026-06-08
Added
Migration-resilient resume: the v2 engine now persists a lightweight per-run progress checkpoint (delivered profiles + per-keyword counts) to the run's key-value store, so after an Apify host migration it resumes only the not-yet-delivered targets instead of restarting from zero. Eliminates the duplicate results previously emitted on migration.
[10.11.4] — 2026-06-08
Changed
Tuned request concurrency for better resource use under load.
[10.11.3] — 2026-06-08
Changed
Daily result limit temporarily lowered from 150,000 to 10,000 while the rebuilt v2 engine is being stabilized (a notice is now shown in the run log). Will be raised over time.
[10.11.2] — 2026-06-05
Added
status + reason on every output record (profiles and companies). Each item now carries status (FOUND / NOT_FOUND) and reason (DOES_NOT_EXIST, UNREACHABLE, or null when found), defaulted at emit time. Non-existent or unreachable handles are now catalogued as NOT_FOUND records instead of vanishing silently: a LinkedIn 404 → DOES_NOT_EXIST, a fetch that exhausts retries → UNREACHABLE, and a company that resolves to no entity → DOES_NOT_EXIST. Both fields are documented in the output schema and shown as the first column in the Profiles/Companies table views.
Fixed
Profiles occasionally returned with only their identity block missing (no firstName/lastName/headline/location/profileId, but experience/education/skills present). The cause: LinkedIn sometimes serves a garbled, partial profile page — not a 404, long enough to look real, but with the top-card identity block absent. That case is now detected (no name and no profile id) and the page is requested again, up to three times. The detection is deliberately conservative — it fires only when both are missing, so a real profile that merely lacks a headline is never retried away. If every attempt still returns a partial page, the actor keeps the partial data rather than dropping the profile.
Profile summary was duplicated. LinkedIn renders the About text twice (a truncated collapsed preview + the full expanded text); the parser joined both, doubling the summary verbatim. It now de-duplicates — dropping any paragraph that is a duplicate of, or a truncated prefix of, a longer one. Confirmed on a 15-profile live run: 0 duplication (e.g. a 537-char doubled summary is now the correct 268 chars).
Profile firstName/lastName missing on some profiles, with the name leaking into headline. Some profile pages carry no firstName/lastName fields at all — the name only exists as the top card's first rendered line. The top-card extractor now isolates that name line (so it no longer leaks into headline) and recovers the name from it when the fields are absent. Live name coverage went 6/15 → 10/15 (every profile that returns a page now gets its name).
[10.11.0] — 2026-06-05
Added
Output schema (.actor/dataset_schema.json + .actor/output_schema.json, wired via actor.jsonstorages.dataset / output). Declares every field the actor can emit (profiles + companies, union, all nullable, nested objects) and gives the Console two dedicated table views — Profiles and Companies. Built from real run outputs, not just the TS interfaces.
README fully rewritten following the Apify Academy best-practice structure: v10 "rebuilt from scratch" header, documented input (all fields + example) and output (maximal profile + company JSON examples covering every emittable field/section), data tables, how-to, pricing, FAQ/legality, and the new banner.
urn (member URN) is now documented in the output schema and README — it was already emitted but undocumented.
Changed
Output field names harmonized between profiles and companies (breaking for the v2 output shape):
followerCount (company) → followersCount (now shared with profiles).
backgroundImageUrl (profile) → coverImageUrl (now shared with companies).
industryName (profile) → industry (now shared with companies).
Distinct-by-design fields kept as-is: profile profilePictureUrl vs company logoUrl, profile summary vs company description.
[10.10.1] — 2026-06-05
Fixed
get-companies returned the wrong company. The company parser picked the first company in the response — which also carries affiliated and similar companies — whenever the name match missed, and that match was case-sensitive, so a slug like Bebity (≠ stored bebity) silently fell through to an unrelated company (it returned Blanche.Agency for Bebity). It now selects the company the query actually resolved to, case-independently.
A company NAME (not a URL) is now searched, not looked up as a slug. Under the legacy isUrl:true flag, a bare token in get-companies (e.g. Bebity) was treated as a direct slug lookup, which resolves to the wrong company. A markerless entry now routes to a proper company search; only an explicit /company/{slug} URL stays a direct lookup. Profile routing is unchanged.
[10.10.0] — 2026-06-05
Added
Experience role descriptions — each experience item now carries a description field (the role blurb / "…see more" text). It is detected structurally, so it is never confused with the location.
Fixed
Profile sections cross-contaminated each other (intermittent, ~1 run in 4). When LinkedIn returned every section inlined into one block, the section boundary matched that single block for all sections, so experience / education / certifications / volunteer each received the full concatenated text and mislabelled it. Sections are now scoped to their own subtree, which holds for both layouts.
location on experiences contained the role description note (and the note was lost when a real location existed). Notes now go to the new description field; location only ever holds a real location.
Company "media / CTA" taglines parsed as fake roles ("Learn more here", promo lines) — they broke multi-role grouping (sub-roles lost their company). They are now recognised structurally and dropped. The same fix removes uploaded certification attachments ("…certificate.pdf") that were emitted as bogus certifications.
Multi-role groups with a standalone employment-type line ("Permanent", "Full-time") lost the company on each sub-role and put the type into location. The type is now read as employmentType (the secondary line before the date; location is the one after).
Blank "+N skills" affordance nodes split experiences — a whitespace-only node was treated as a job title, swallowing the next real role. Whitespace-only text is now ignored everywhere.
Education "Activities and societies" became a bogus school entry (or was silently dropped when long). It is now captured as the entry's activities field.
Honors & Projects emitted noise entries — the "Associated with {company/school}" association line and the "Other contributors" avatar affordance were parsed as separate entries (and the association line stole the real entry's description). Both are now skipped.
Skills empty-state placeholder ("Nothing to see for now" / "Skills that X adds will appear here.") was emitted as two skills on profiles with no skills. Now filtered.
A typed name submitted with the legacy isUrl:true flag was treated as a direct profile slug instead of a search (e.g. a typed first-and-last name was fetched as /in/{that name}, which exists for nobody). Under isUrl:true, only entries that can actually be a slug (URL marker or a space-free token) are routed to a direct lookup; anything else is a search.
[10.9.0] — 2026-06-04
Fixed
v1-shaped inputs were silently ignored, returning the schema default (keywords:["web dev"], limit:1) instead of the submitted targets. Root cause: the schema exposed its fields under v2__-prefixed names, so Apify injected the v2__*defaults for every field a v1-shaped input (action/keywords/limit) didn't provide. Those injected defaults flipped normalizeInput's isV2 discriminator to true (v2-wins precedence), so the engine read the empty v2__search:["web dev"] / v2__limit:1 defaults and discarded the real keywords/limit. Net effect: a run of 4 profile URLs with limit:4 searched "web dev" capped at 1 and emitted a single record.
Changed
Input fields renamed back to the shared v1/v2 names (v2__action→action, v2__search→keywords, v2__limit→limit, v2__location→location, v2__profileFields→profileFields, v2__proxyConfiguration→proxyConfiguration). There is now a single set of field names, so normalizeInput and the startup-summary config read them directly — the v2__ vs v1 discriminator (and the v2-wins precedence that caused the bug) is gone. Legacy isUrl:true is still honored as a direct-lookup hint; isName remains a no-op. Behavior is otherwise unchanged.
[10.8.0] — 2026-06-03
Changed
Simplified v2 input. Targeting collapses from four concepts (action + isUrl + isName + the implicit meaning of keywords) to two: v2__action (Profiles / Companies) and v2__search (one bulk list of search terms, URLs, or /in/·/company/ slug-paths). URLs are auto-detected; a bare term is a search; a /in/{slug} or /company/{slug} marker forces a direct lookup (returns one result, ignores the limit). Removed the confusing isUrl/isName toggles and the enrichWith* fields from the v2 UI. The location field becomes v2__location (same autocomplete behavior).
Added
normalizeInput backward-compat layer (src-v2/dtos/normalize-input.ts). All v2 fields are prefixed v2__; the presence of any v2__ key marks an input as v2. Legacy v1-shaped inputs (action/keywords/isUrl/isName) still work unchanged — they ride through the schema's additionalProperties: true and are mapped to the canonical model (isName is dropped, being a no-op in v2). Raw LinkedIn IDs are rejected with a clear message instead of issuing a dead request. The schema requires nothing (required: []) so v1-shaped inputs still pass platform validation. Covered by offline tests in src-v2/__tests__/run.ts.
[10.7.0] — 2026-06-03
Added
location filter now works (get-profiles). The previously-inert "🌍 Location" input is wired into people-search. Each entry is resolved autocomplete-style: free text (e.g. Paris, United States, Greater London) is matched against LinkedIn's own location suggestions and the top match is used; a raw geo id (106383538), a urn:li:geo:… URN, or a URL carrying geoUrn=… is used directly (no lookup). Multiple entries widen the filter (matches ANY). The filter is resolved once at startup and rides every search request, pagination included. Unmatched entries are skipped with a warning (the run continues; if none resolve it runs unfiltered) — the matched place names are surfaced in the run log. Profiles only — company search is unaffected.
[10.6.0] — 2026-06-03
Fixed
limit ("🔢 Limit of result per query") no longer caps directly-provided URLs, and is now per-query. It was implemented as a single global counter applied to every queued item — including profile/company URLs passed in directly. With the default limit=1, a run of N URLs (e.g. the big-one preset: 493 profile URLs) returned only 1 item. Now: direct URLs bypass the per-query cap (all N are scraped, still subject to the daily quota), and in search mode limit is a per-keyword budget — limit=5 over 2 search terms yields up to 10 rows, matching the input-schema description. It composes with the daily-quota cap from 10.5.0 (per-query limit AND global remaining quota both apply).
Added
profileFields multi-select (get-profiles) — choose which profile sections to scrape. Each option costs one extra request per profile, so fewer selections = faster and cheaper runs. Empty/absent = scrape everything (backward compatible). Base identity (name, headline, location, picture, counts) is always returned.
Company output enriched (get-companies): CompanyData now also exposes phone ({ number, extension }), callToAction ({ text, type, url }), verified (page verification), active, pageType (COMPANY/SCHOOL/SHOWCASE), jobSearchUrl, and hashtags (associated #tags, leading # stripped). Field shapes validated against LinkedIn's own models and covered by offline tests.
[10.5.0] — 2026-06-02
Added
Per-user daily result quota in v2, ported from v1. Counts delivered dataset items (profiles and companies, one shared quota) per Apify userId over a 24h sliding window. Configurable through DAILY_LIMIT (default 150000), and shared with v1 so both draw from the same budget.
The run's effective cap is min(input.limit, remaining); the run stops queuing work at that cap, so the daily limit is never exceeded. Each emitted item counts once.
Graceful degradation: if the quota store is unreachable, the limiter steps aside and the run proceeds unthrottled.
A quota line in the run log (Daily quota: X/Y used today — Z remaining); everything else about the counter stays on the debug channel.
Changed
Unlike v1 (which fails the run), hitting the daily cap now lets the run succeed: allowed results are delivered and the run finishes with a warning + an Apify status message (Daily limit … Resets in ~N min). A run that is already at the cap exits immediately.
[10.4.0] — 2026-06-02
Added
action=get-companies in v2 — company search and company details (previously threw). Company search mirrors people-search and collects /company/{universalName} slugs; company details are one request per company → one dataset item.
Offline tests over real captured responses: parser fields and slug extraction.
[10.3.1] — 2026-06-02
Changed
The engine's internal work queue is never written to the run's storage. v10.3.0 wrote it during the run and deleted it in a finally block — so it was visible mid-run and the cleanup was best-effort. It is now held in memory only. Scoped to that queue: the dataset output persists exactly as before, and general-debug mode still keeps the queue for the Apify crawler UI.
[10.3.0] — 2026-06-02
Added
General-debug mode (src-v2/): activate via the GENERAL_DEBUG=1 env var or a hidden input field. When on, the crawler's stats and state are exposed in the Apify UI; when off, none of it is.
Two log streams: a friendly, safe run log by default; a detailed one under general-debug. All levels (debug/info/warning/error).
Startup summary: every run opens with the scraper version, log mode, general-debug state, action/limit/keyword counts, and a warning for any missing required configuration.
Changed
Engine internals now log on the private channel only; the default run log carries the user-facing narrative.
The default run log no longer carries the crawler's own chatter — routine retry warnings that expose the target URL and retry count. A genuine fatal error still surfaces. Private mode shows everything.
Fixed
Per-second "Statistics" log spam in public mode. The previous suppression trick set the interval to ~41 years; multiplied out it overflowed a timer's 32-bit signed range and silently clamped to ~1 ms, flooding the log dozens of times per second with a line that leaked engine internals. Suppression is now done by log level; the interval stays a sane value (60s public / 30s private).
The work queue no longer lingers in the run's storage in public mode. Other engine state was already gated out of the Key-Value store, but the queue is a separate store and stayed behind. It is now dropped at shutdown when general-debug is off — the dataset output is untouched, and private mode keeps it for the Apify crawler UI.
[10.2.1] — 2026-06-02
Fixed
Idempotent profile seed: a re-queued profile no longer resets its live accumulator, which used to dedupe its own parts away and strand the profile so it never emitted.
Unresolved section requests are filtered out defensively (the project's TypeScript is deliberately loose).
[10.2.0] — 2026-06-02
Added
Unified single-queue v2 pipeline. Every LinkedIn call (search → profile → sections → detail pages) is now one queued request instead of a direct first phase plus an inline second one, and a profile's follow-up calls run concurrently. Output unchanged: one dataset item per profile.
Two-wave profile accumulator (src-v2/linkedin/profile-accumulator.ts): a per-profile completion barrier that emits one aggregated item, best-effort — a part that exhausts its retries still lets the profile through.
Changed
Search retries now ride the crawler's own retry policy instead of a hand-rolled loop.
Profile handling split into shell / section / detail-page handlers plus an extractor registry.
Fixed
A per-(profile, part) key on section requests stops the crawler deduping one profile's parts against another's — their URLs are identical, the profile lives in the request body.
[10.1.0] — 2026-06-02
Added
proxyConfiguration input (standard Apify proxy editor) — supports both Apify Proxy (groups/country) and custom proxy URLs.
Sections behind a "Show all" link are now loaded, generically: the engine reads which sections a profile defers and requests each one, instead of special-casing a single section.
skills wired + verified live (6–57 clean skills).
certifications wired (prefers the complete list over the card's inline preview) + verified live ({name, issuer, date}).
honors / projects / organizations / languages wired + verified live via a generic entity grouper: new ProfileData fields honors ({title, description}), projects ({name, dateRange, description}), organizations ({name, role, dateRange, description}); languages now come from the same path.
courses/publications: recipes ready, parsers still to add. volunteer/patents: still to capture.
Offline test harness (pnpm run test:v2, src-v2/__tests__/): assertions over real captured responses (request shape, section discovery and parsing, top-card name/headline/location).
New profile fieldsprofilePictureUrl + backgroundImageUrl: the member's own images, scoped to the top card so a recommendation's or a company's avatar is never picked up, at the largest available rendition (≈800×800 photo, 350×1400 cover).
followersCount / connectionsCount, read from the top card's rendered text — they are not numeric fields in the response. Handles both the single-line ("255 followers") and split ("229" + "connections") forms, and "500+". Verified live: followers on creator profiles, connections on normal profiles.
Changed
pnpm start now runs v2 (src-v2/main.ts) as the classic Apify actor entry (also used by apify run). v1 stays reachable via pnpm run start:v1. Added build:v2.
Section requests now carry only what LinkedIn's own client sends. They no longer depend on the profile id being read first, so a missed extraction no longer zeroes the profile — it is a health signal, not a gate.
Fixed
Top-card parsing: headline/name/location were mis-assigned (name or company used as headline; a comma-bearing headline classified as location; pronouns "She/Her" used as headline; comma-less locations dropped). Now parsed by stable document order — verified live on four profiles covering each of those shapes.
firstName/lastName now have a fallback for the large pages where the tree walk misses the profile shape.
Top-card name with a nickname (e.g. "Zsófia Réka (Sophie) Tóth"): the exact full-name match failed and the headline became the name; it now falls back to a starts-with-first + contains-last match to skip the name line.
Notes
Known gaps: skills, certifications, honors, projects, organizations and languages now work. Remaining: courses, publications (recipe ready, parser to add) and volunteer/patents. industryName is not served on this path; followersCount/connectionsCount are read from the top card. languages name/proficiency pairing is fragile when a language has no proficiency level.
[10.0.0] — 2026-06-02
Added
v2 scraping engine (src-v2/) talking directly to LinkedIn's modern interface — no dependency on @bebity/linkedin-scraper.
Two-phase profile pipeline: keyword/URL search → vanity names, then one request per profile plus one per section (summary, experience, education, certifications, volunteer, languages, skills).
Documentation under docs/ (ARCHITECTURE.md, DATA-SOURCING.md).
Changed
Version set to 10.0.0 to mark the v2 rewrite (previously tracked the @bebity/linkedin-scraper package version, 7.x).
Notes
Input/output and published actor identity stay compatible with v1.
v2 currently supports profiles only (get-profiles); get-companies still throws.
enrichWithCompany / enrichWithContact are accepted in the input schema (v1 compat) but not yet implemented in v2.
v1 engine (src/, wrapping @bebity/linkedin-scraper) remains the deployed/shipping path.