API Docs Get API key
Python Tutorial

How to Scrape a LinkedIn Profile in Python

Pull a profile's name, title, company, and location into clean structured data with a few lines of Python — no headless browser, no proxies, and no account-ban risk. Each call is 1 credit.

"Scraping a LinkedIn profile in Python" usually means one thing: you have a profile URL (or a name, or an email) and you want the structured data behind it — full name, headline, job title, company, location — in a dict you can drop into a database or CRM.

The DIY route is harder than it looks. LinkedIn blocks unauthenticated requests, renders profiles with JavaScript so plain requests + BeautifulSoup returns almost nothing, rate-limits and bans datacenter IPs, and — most importantly — automating a logged-in account is the fastest way to get that account restricted. Maintaining a Selenium fleet with rotating proxies is a project in itself, and a brittle one.

This tutorial takes the reliable path instead: a single REST endpoint that returns the parsed profile for a URL. No browser to drive, no session to keep alive, no ban risk on your own account. Everything below is Python-first (with cURL and Node alternates), runs against POST https://api.linkfinderai.com, and costs 1 credit per call — including lookups that come back empty. The free tier's 100 credits is enough to follow the whole guide.

Why the hard way breaks. A naive scrape like the snippet below doesn't get you profile data — it gets you blocked. LinkedIn returns an HTTP 999 or a login wall for unauthenticated requests, the real content is JS-rendered, and scripting a logged-in session risks your account. The API approach sidesteps all of it.
import requests

# Plain GET against a public profile — this does NOT work
resp = requests.get("https://www.linkedin.com/in/john-doe")
print(resp.status_code)   # 999  (LinkedIn blocks the request)
print("name" in resp.text)  # False — content is JS-rendered behind auth
1

Get your API key

Create a free account and copy your key from the dashboard under Settings → API Key. You get 100 credits to start — enough to scrape ~100 profiles while you test.

Keep the key out of your code. Set it as an environment variable and read it at runtime so it never lands in a commit or a notebook you share.

# Add to your shell profile or a .env file (never commit it)
export LINKFINDER_API_KEY="lf_live_your_key_here"
import os
API_KEY = os.environ["LINKFINDER_API_KEY"]
BASE_URL = "https://api.linkfinderai.com"
2

Scrape your first profile

One POST, two fields. Set type to linkedin_profile_to_linkedin_info and pass the profile URL as input_data. The API does the heavy lifting server-side and hands you parsed JSON — no browser, no proxy, no login.

import os, requests

API_KEY = os.environ["LINKFINDER_API_KEY"]

resp = requests.post(
    "https://api.linkfinderai.com",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "type": "linkedin_profile_to_linkedin_info",
        "input_data": "https://linkedin.com/in/john-doe",
    },
    timeout=30,
)

data = resp.json()
if data["status"] == "success":
    profile = data["result"]
    print(profile["full_name"], "—", profile["job_title"], "@", profile["company_name"])
else:
    print("No data:", data.get("message"))
curl -X POST "https://api.linkfinderai.com" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer $LINKFINDER_API_KEY" \
     -d '{
       "type": "linkedin_profile_to_linkedin_info",
       "input_data": "https://linkedin.com/in/john-doe"
     }'
const API_KEY = process.env.LINKFINDER_API_KEY;

const resp = await fetch("https://api.linkfinderai.com", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "linkedin_profile_to_linkedin_info",
    input_data: "https://linkedin.com/in/john-doe",
  }),
});

const data = await resp.json();
if (data.status === "success") {
  const p = data.result;
  console.log(`${p.full_name} — ${p.job_title} @ ${p.company_name}`);
}
The same endpoint scrapes a company page too — swap the type to linkedin_company_to_linkedin_info and pass a https://linkedin.com/company/... URL.
3

Read the profile data

Every response wraps your data in a status + result envelope. Branch on status first — a 200 can still come back "error" with a null result if the profile couldn't be resolved. On success, result is the parsed profile:

{
  "status": "success",
  "result": {
    "full_name": "Sarah Mitchell",
    "job_title": "VP of Sales",
    "seniority_level": "vp",
    "company_name": "CloudCore",
    "company_domain": "cloudcore.io",
    "company_size": "51-200",
    "industry": "Software",
    "city": "Austin",
    "state": "Texas",
    "country": "United States",
    "linkedin": "https://linkedin.com/in/sarah-mitchell-sales"
  }
}
{
  "result": null,
  "status": "error",
  "message": "Profile not found"
}

From here it's plain Python — index into the dict, write to a CSV, or insert into your database. The exact field list lives in the API docs; treat any field defensively with .get() since not every profile exposes every attribute.

A lookup that returns null still costs 1 credit. De-duplicate your URL list and cache results so you never pay twice for the same profile.
4

Scrape from just a name or email

Often you don't have the profile URL yet — only a name and company, or an email from a signup form. Resolve the URL first, then scrape it. A small helper keeps every call to one line:

import os, requests

API_KEY = os.environ["LINKFINDER_API_KEY"]
BASE_URL = "https://api.linkfinderai.com"

def call(enrichment_type, input_data, **extra):
    """One request = one credit. Returns the result or None."""
    resp = requests.post(
        BASE_URL,
        headers={"Authorization": f"Bearer {API_KEY}",
                 "Content-Type": "application/json"},
        json={"type": enrichment_type, "input_data": input_data, **extra},
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    return data.get("result") if data.get("status") == "success" else None

# From a name + company
url = call("lead_full_name_to_linkedin_url", "Sarah Mitchell CloudCore")

# ...or reverse-lookup the URL from an email
url = url or call("email_to_linkedin_url", "[email protected]")

# Then scrape the profile behind it
profile = call("linkedin_profile_to_linkedin_info", url) if url else None
print(profile)

That's the building block for the rest of the guide — every scrape is just call("linkedin_profile_to_linkedin_info", some_url).

5

Scrape a list of profiles in bulk

Reading a CSV of profile URLs and scraping each row is the common job. Two things matter at volume: stay under your plan's requests-per-second limit, and retry transient failures instead of crashing halfway through. Here's a bulk scraper that does both.

import os, csv, time, requests

API_KEY = os.environ["LINKFINDER_API_KEY"]
BASE_URL = "https://api.linkfinderai.com"

INPUT = "profiles.csv"        # one column: linkedin_url
OUTPUT = "profiles_scraped.csv"
REQS_PER_SEC = 5              # match your plan (Starter = 5/s)
DELAY = 1.0 / REQS_PER_SEC

def scrape(url, max_retries=4):
    delay = 1.0
    for _ in range(max_retries):
        resp = requests.post(
            BASE_URL,
            headers={"Authorization": f"Bearer {API_KEY}",
                     "Content-Type": "application/json"},
            json={"type": "linkedin_profile_to_linkedin_info",
                  "input_data": url},
            timeout=30,
        )
        if resp.status_code in (429, 500):   # back off: 1s -> 2s -> 4s -> 8s
            time.sleep(delay); delay *= 2; continue
        if resp.status_code == 402:
            raise RuntimeError("Out of credits — top up to continue.")
        resp.raise_for_status()
        data = resp.json()
        return data.get("result") if data.get("status") == "success" else None
    return None  # gave up after repeated rate limiting

FIELDS = ["full_name", "job_title", "company_name", "industry", "city", "country", "linkedin"]

with open(INPUT, newline="") as f_in, open(OUTPUT, "w", newline="") as f_out:
    reader = csv.DictReader(f_in)
    writer = csv.DictWriter(f_out, fieldnames=FIELDS, extrasaction="ignore")
    writer.writeheader()

    for row in reader:
        profile = scrape(row["linkedin_url"])
        if profile:
            writer.writerow(profile)
            print("scraped:", profile.get("full_name"))
        else:
            print("skipped:", row["linkedin_url"])
        time.sleep(DELAY)     # stay under the rate limit

print("Done.")

The codes you'll actually hit, and how the script handles each:

CodeMeaningWhat to do
200SuccessCheck result — may be null (still 1 credit)
401UnauthorizedMissing/invalid key — check the Authorization header
402Insufficient creditsTop up or wait for the next billing cycle
422Invalid requestVerify the type and input_data format
429Rate limit exceededBack off exponentially: 1s, 2s, 4s
500Server errorRetry after ~30s; contact support if it persists
PlanCredits / moRequests / secBatch size
Starter5,0005 req/sUp to 500
Professional20,00010 req/sUp to 500
Enterprise50,00020 req/sUp to 500
HyperGrowth250,00050 req/sUp to 500
6

Also pull email, phone & company data

Scraping the profile gives you the firmographics. To turn it into a contactable record, add two more one-credit calls off the same URL — and pull the full company page if you want headcount and industry detail:

# Reuse the call() helper from Step 4
url = "https://linkedin.com/in/sarah-mitchell-sales"

record = {
    "profile": call("linkedin_profile_to_linkedin_info", url),
    "email":   call("linkedin_profile_to_email", url),   # verified work email
    "phone":   call("linkedin_profile_to_phone", url),   # direct number
}

# Optional: scrape the company page behind the profile
domain = record["profile"].get("company_domain") if record["profile"] else None
if domain:
    record["company"] = call("company_name_to_employee_count", domain)

print(record["email"], record["phone"])

That's four calls (four credits) for a complete contact: full profile, email, phone, and company size. Each is the same POST with a different type — there's no separate endpoint to learn.

Need to scrape and source at once? The leads_finder_ai endpoint takes a plain-English description ("VP Sales at B2B SaaS in the US") and returns up to 100 fully-scraped profiles in a single call — see the docs.

Start scraping profiles in Python today

Skip the proxies, the headless browsers, and the banned accounts. Get structured LinkedIn profile data from one REST call — 100 free credits, an API on every plan, flat 1-credit pricing.

Get your API key

No credit card required • API on every plan • Flat pricing • Cancel anytime