How to Scrape LinkedIn: A Complete Guide (incl. Avoiding Blocks)
- A plain
requests.getonlinkedin.com/in/<handle>with no User-Agent returned HTTP 999 (LinkedIn's denial code) for me. With a Chrome User-Agent I got HTTP 200 but a sign-in wall where the full profile should have been. - LinkedIn blocks on IP reputation, request fingerprint, and behavior. There is no public people or profile API, so logged-out HTML is the only DIY surface and it is heavily gated.
- Three setups that work: a logged-in browser session with Selenium plus slow human-like pacing, residential proxies with a real headless browser, or a scraper API that returns parsed JSON and handles blocks for you.
- Scraping public LinkedIn data is treated as legal in the US under hiQ v. LinkedIn, but LinkedIn's User Agreement still prohibits it, so the account risk is real. Details here.
I tried to scrape LinkedIn the lazy way first: one requests.get against a public profile, no browser, no proxy. It came back HTTP 999 before I could parse a single field. That 999 is the whole story of scraping LinkedIn, because it is what nearly everyone hits, and the standard advice (set a User-Agent) only swaps one failure for another.
Below is exactly what I ran in June 2026, the status codes LinkedIn returned, why the blocks happen, and the three setups that actually get LinkedIn data back. Every code sample is Python I executed myself.
Can you scrape LinkedIn at all?
You can scrape publicly visible LinkedIn data, but two hard constraints shape how. First, LinkedIn has no public API that returns other people’s profiles, companies, or job listings. The Consumer and Profile APIs only expose your own account fields after OAuth, and the richer Marketing and partner APIs require approval into the LinkedIn Partner Program. So the question “how do I scrape my LinkedIn API” has an awkward answer: there is no general people API to call. The only DIY data surface is the logged-out HTML page.
Second, that HTML page is gated. LinkedIn detects automated clients and serves a sign-in wall instead of the data. So “can you scrape LinkedIn data” splits into two outcomes that I measured directly:
| Request | User-Agent | HTTP status | Body size | Usable profile data |
|---|---|---|---|---|
GET /in/williamhgates | none | 999 | ~1.5 KB | No (denied) |
GET /in/williamhgates | Chrome desktop | 200 | ~750 KB | No (sign-in wall) |
The 200 looks like a win until you parse it. The page contained one <script type="application/ld+json"> block, but it held Article feed teasers. The person’s name, headline, and experience were absent, and the markup carried a “sign in to view” prompt. LinkedIn returned a page, and the profile data stayed behind the wall. That gap between “got a 200” and “got the data” is where most LinkedIn scrapers quietly fail, and it sets up the next question: what is LinkedIn actually blocking on?
Why does LinkedIn block scrapers?
LinkedIn blocks scrapers on IP reputation, request fingerprint, and behavioral signals, layered so that fixing one does not clear the others. The first layer is the User-Agent and IP. A request with no User-Agent, or one identifying as python-requests or curl, gets denied immediately. In my run, the no-User-Agent request returned HTTP 999, LinkedIn’s non-standard “Request Denied” code documented on Wikipedia’s HTTP status list and at http.dev. The 999 is a hard tell that the request never reached the data tier.
The second layer is request fingerprinting. Scrapfly’s teardown of LinkedIn’s defenses notes that LinkedIn evaluates a fraud score built from IP quality, the TLS JA3 fingerprint, HTTP header order, and device attributes. A datacenter IP with a clean User-Agent still scores poorly because the TLS handshake and header shape do not match a real browser. This is why swapping in a Chrome User-Agent got me a 200 page with no profile in it: the request passed the cheap check and failed the expensive one.
The third layer is behavioral. Once you are logged in, LinkedIn watches pace and pattern. Security researchers describe browser fingerprinting, rate-based heuristics, and IP reputation scoring running in real time to separate networking from harvesting. Viewing 100 profiles a minute, hitting profiles in sequential order, or sending identical actions at machine speed flags the account. The practical ceiling teams report is roughly 100 to 200 profile views per day per account with multi-second gaps.
These three layers are why LinkedIn scraping limitations are not solved by one trick. The fix has to change the IP, the fingerprint, and the pacing together, which is what the working setups below do.
How do you scrape LinkedIn data with Python?
The most reliable free way to scrape LinkedIn data with Python is to drive a real logged-in Chrome session with Selenium, because a genuine browser passes the fingerprint checks that block plain HTTP. The widely used open-source library for this is linkedin_scraper (MIT licensed), which wraps the Selenium calls and the page selectors for you.
First, here is the naive request that fails, so you can recognize the 999 when you see it:
import requests
# This returns HTTP 999 from LinkedIn. Do not build on it.
r = requests.get("https://www.linkedin.com/in/williamhgates", timeout=20)
print(r.status_code) # -> 999
print(len(r.text)) # -> ~1530 bytes, a denial page, no profile
Adding a Chrome User-Agent changes the status code to 200, and the response is still the sign-in wall with the person’s data held behind it. To get past that, you need an authenticated browser session. Here is the Selenium route using linkedin_scraper:
# pip install linkedin_scraper selenium
from selenium import webdriver
from linkedin_scraper import Person, actions
driver = webdriver.Chrome()
# Log in with your own account. This drives a real browser session,
# which is what clears LinkedIn's fingerprint checks.
actions.login(driver, "you@example.com", "your_password")
person = Person("https://www.linkedin.com/in/williamhgates", driver=driver)
print(person.name) # -> "Bill Gates"
print(person.job_title) # current headline
for exp in person.experiences: # work history
print(exp.institution_name, "-", exp.position_title)
driver.quit()
The Person class loads the profile in the live browser, then reads fields with XPath selectors against the rendered DOM. A Company class works the same way for company pages, exposing name, about_us, website, headquarters, and company_size. Because this runs through your logged-in session, every request counts against your own account, so the daily-view ceiling and the behavioral pacing from the section above apply directly.
This works for small jobs. It also has three failure modes I keep running into.
Where the Selenium approach breaks
The Selenium-plus-login approach breaks on markup churn, account risk, and scale. LinkedIn changes its DOM structure often, and when a span or class moves, the XPath selector returns nothing and the field comes back empty with no error. You find out when your data has blank columns.
The account risk is the bigger one. You are automating your own logged-in account, which is exactly what LinkedIn’s behavioral layer is built to catch. Push past the informal daily ceiling and the account gets a temporary restriction or a permanent ban. There is no safe way to run this headless across thousands of profiles on a single login.
Scale is the third wall. One browser session is sequential and slow. Parallelizing means multiple logged-in accounts and multiple residential IPs, which turns a script into an infrastructure project. That tradeoff between a quick script and a maintained system is the line where most teams switch approaches.
How do you avoid getting blocked when scraping LinkedIn?
You avoid LinkedIn blocks by changing the IP reputation, the browser fingerprint, and the request pace together, since LinkedIn scores all three. These are the levers that moved the result in my testing and in the practitioner reports, in rough order of impact:
- Use residential or mobile IPs. Datacenter ranges are pre-scored as low quality and draw the 999 fast. Residential proxies present as ordinary home connections, which is the single biggest factor in getting a real page back.
- Drive a real browser. A headless Chrome or Playwright session produces a browser-shaped TLS handshake and header order, so it survives the fingerprint check that kills raw HTTP. Plain
requestsfails this regardless of the User-Agent you set. - Slow down and randomize. Practitioners report keeping a logged-in account under roughly 100 to 200 profile views per day with 3 to 5 second gaps and randomized timing. Sequential, machine-paced access is the clearest bot signal.
- Keep sessions warm and stable. Reuse one authenticated session with consistent cookies and avoid logging in repeatedly. Repeated fresh logins from new fingerprints look hostile.
- Respect the gated surface. LinkedIn’s robots.txt opens with “The use of robots or other automated means to access LinkedIn without the express permission of LinkedIn is strictly prohibited” and disallows
/in/,/pub/, and search paths for general bots. Ignoring it is part of what gets an IP flagged.
The honest tradeoff: doing all of this yourself means buying a residential proxy pool, running and rotating headless browsers, refreshing sessions, and repairing selectors every time the DOM shifts. That is a standing maintenance job once you pass a few hundred profiles, which is why most teams hand the blocking problem to a scraper API.
How do you scrape LinkedIn at scale without managing proxies?
A LinkedIn scraper API removes the blocking work by taking a LinkedIn URL and returning parsed JSON, with the residential proxies, browser rendering, fingerprinting, and retries handled server-side. You send one request and get structured fields back, no 999 to debug and no account of yours on the line.
In my runs the ChocoData LinkedIn endpoint returned a profile as clean JSON from a single call:
curl "https://chocodata.com/api/v1/linkedin/profile?url=https://www.linkedin.com/in/williamhgates&api_key=$CHOCO_API_KEY"
The same pattern in Python, which is what I actually ran into a script:
import requests
CHOCO_API_KEY = "your_api_key" # from app.chocodata.com/sign-up
resp = requests.get(
"https://chocodata.com/api/v1/linkedin/profile",
params={
"url": "https://www.linkedin.com/in/williamhgates",
"api_key": CHOCO_API_KEY,
},
timeout=60,
)
data = resp.json()
# Parsed fields come back directly, no XPath, no authwall
print(data["name"])
print(data["headline"])
print(data["location"])
for job in data["experience"]:
print(job["title"], "at", job["company"])
When you need many profiles at once, the same endpoint scales cleanly with asyncio, because each request is independent and the rotation happens server-side. Here is an async batch I ran across a list of profile URLs:
import asyncio
import httpx
CHOCO_API_KEY = "your_api_key"
PROFILES = [
"https://www.linkedin.com/in/williamhgates",
"https://www.linkedin.com/in/satyanadella",
]
async def scrape_one(client, url):
r = await client.get(
"https://chocodata.com/api/v1/linkedin/profile",
params={"url": url, "api_key": CHOCO_API_KEY},
timeout=60,
)
person = r.json()
return person["name"], person["headline"]
async def main():
async with httpx.AsyncClient() as client:
tasks = [scrape_one(client, u) for u in PROFILES]
for name, headline in await asyncio.gather(*tasks):
print(name, "-", headline)
asyncio.run(main())
Each await returns one parsed profile element, so a list of thousands of URLs collects concurrently without you touching a single proxy or browser. Because the proxy rotation and the browser session live on the server, the per-account daily ceiling stops being your limit, and your own LinkedIn login never touches the job. The same approach covers the other LinkedIn objects through dedicated endpoints: a LinkedIn profile scraper, a company scraper, a job scraper for postings, and a search results scraper for query pages. Each accepts a URL and returns the parsed fields.
For a one-off pull of a handful of profiles, the free Selenium route is fine and costs nothing but your time and a little account risk. For continuous collection across thousands of profiles, companies, or jobs, offloading the IPs, the rendering, and the retries is the cheaper path once you price in maintenance and the cost of a banned account.
Before you collect anything at volume, it is worth knowing where the legal line sits. Scraping public LinkedIn data was treated as lawful under the Computer Fraud and Abuse Act in hiQ v. LinkedIn, but the same case ended with hiQ losing on breach of LinkedIn’s User Agreement and paying $500,000. I unpack what that split means for you in is scraping LinkedIn legal and in my breakdown of LinkedIn’s Terms of Service. If you want the deeper Python build, see how to scrape LinkedIn with Python.
FAQ
Can I scrape data from LinkedIn?
You can scrape publicly visible LinkedIn data, and the Ninth Circuit in hiQ v. LinkedIn held that scraping public data does not violate the Computer Fraud and Abuse Act. LinkedIn's User Agreement still prohibits scraping with software, bots, or crawlers, so doing it can get an account restricted. LinkedIn also has no public API that returns other people's profiles, so the only DIY surface is logged-out HTML, which is rate-limited and gated behind a sign-in wall.
How can I scrape data from LinkedIn for free?
The free route is the open-source linkedin_scraper library driving a real Chrome session through Selenium, which I cover below. It works for small volumes but needs your own logged-in account and breaks when LinkedIn changes its markup or shows the authwall. Free does not mean safe: automating your own account is the fastest way to a restriction.
Why did my LinkedIn request return HTTP 999?
HTTP 999 is LinkedIn's non-standard 'Request Denied' code, listed on Wikipedia's HTTP status code page. It fires when the request looks automated: a non-browser User-Agent like python-requests, a datacenter IP, or too many requests from one address. In my test, a request with no User-Agent returned 999 in 1.5 KB while a Chrome User-Agent returned HTTP 200.
Can ChatGPT or Claude scrape LinkedIn?
No. ChatGPT and Claude cannot scrape LinkedIn directly because LinkedIn blocks their crawlers and serves a sign-in wall to logged-out clients. An AI assistant can write scraping code for you, but the code still has to solve the same IP, fingerprint, and authwall problems. Connecting an LLM to a scraper API that returns parsed JSON is the practical path.
How many LinkedIn profiles can I scrape per day?
There is no published LinkedIn scraping rate limit, because LinkedIn does not sanction scraping. Practitioners widely report keeping a logged-in account under roughly 100 to 200 profile views per day with multi-second gaps to avoid triggering a restriction. A scraper API moves that risk off your account by rotating its own IP pool, so the per-account ceiling stops being your bottleneck.