<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[EnrichGrid Blog]]></title><description><![CDATA[EnrichGrid Blog]]></description><link>https://enrichgrid.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aed10eef9805003095221e7/47ed222f-b20b-47e1-b5da-2c4d33c1f843.png</url><title>EnrichGrid Blog</title><link>https://enrichgrid.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 11:11:28 GMT</lastBuildDate><atom:link href="https://enrichgrid.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Proxycurl Alternative in 2026: How to Get LinkedIn Profile and Company Data with an API (Python)]]></title><description><![CDATA[Proxycurl shut down. Get LinkedIn profile and company data with Python: working code, a field mapping, bulk enrichment and rate limits.]]></description><link>https://enrichgrid.hashnode.dev/proxycurl-alternative-in-2026-how-to-get-linkedin-profile-and-company-data-with-an-api-python</link><guid isPermaLink="true">https://enrichgrid.hashnode.dev/proxycurl-alternative-in-2026-how-to-get-linkedin-profile-and-company-data-with-an-api-python</guid><category><![CDATA[Python]]></category><category><![CDATA[api]]></category><category><![CDATA[LinkedIn]]></category><category><![CDATA[web scraping]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[EnrichGrid]]></dc:creator><pubDate>Thu, 05 Nov 2026 23:38:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aed10eef9805003095221e7/69cddf4e-c349-437a-b122-0fe4b92d3ef9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If your CRM sync, recruiting tool or lead-scoring pipeline called Proxycurl, it broke in July 2025 when Proxycurl shut down after LinkedIn's lawsuit. Since then a lot of teams have been looking for a replacement that returns the same kind of data: a LinkedIn URL goes in, a structured professional profile comes out.</p>
<p>This guide covers:</p>
<ol>
<li>Your options in 2026, and their trade-offs</li>
<li>What data you can get from a LinkedIn URL</li>
<li>Working Python code for profiles, companies and people search</li>
<li>A Proxycurl → new API field mapping so your migration is a find-and-replace, not a rewrite</li>
<li>Bulk enrichment of thousands of URLs, with rate limits and retries</li>
<li>FAQ: legality, freshness, emails, cost</li>
</ol>
<p>The examples use <a href="https://enrichgrid.com/?ref=hashnode">EnrichGrid</a>, the API I work on. The patterns (polling, retries, concurrency, field mapping) apply to any provider.</p>
<hr />
<h2>Your options in 2026</h2>
<table>
<thead>
<tr>
<th>Option</th>
<th>Profile data?</th>
<th>Effort</th>
<th>Main risk</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Official LinkedIn API</strong></td>
<td>Only the signed-in member's own profile, or partner programs with approval</td>
<td>Apply, wait, hope</td>
<td>Partner access is rarely granted for enrichment</td>
</tr>
<tr>
<td><strong>Build your own scraper</strong></td>
<td>Yes</td>
<td>High: proxies, logins, captchas, constant breakage</td>
<td>Accounts get restricted, and maintenance never ends</td>
</tr>
<tr>
<td><strong>Browser extensions that scrape inside your session</strong></td>
<td>Yes</td>
<td>Low</td>
<td>They act through your LinkedIn account</td>
</tr>
<tr>
<td><strong>Data APIs</strong> (EnrichGrid, People Data Labs, Coresignal, Bright Data datasets…)</td>
<td>Yes</td>
<td>Low: one HTTP call</td>
<td>Varies by vendor: freshness, pricing model, coverage</td>
</tr>
</tbody></table>
<p>For most products, a data API is the practical choice: no LinkedIn account involved, no scraping infrastructure, predictable JSON. When you compare vendors, check four things:</p>
<ul>
<li><strong>Freshness.</strong> Is the profile collected live or from a months-old dump?</li>
<li><strong>Pricing model.</strong> Are you charged per <em>request</em> or per <em>found profile</em>? On real-world lists, 10–30% of URLs are outdated, so this decides your bill.</li>
<li><strong>Field coverage.</strong> Do you get full work history, or only the current job?</li>
<li><strong>Minimums.</strong> Annual contracts and seat fees vs pay-as-you-go.</li>
</ul>
<hr />
<h2>What data you can get from a LinkedIn URL</h2>
<p>A person lookup returns, for a typical public profile:</p>
<ul>
<li>name, headline, summary, location and country</li>
<li>current title, <strong>seniority</strong> (C-level, VP, director…) and <strong>job function</strong></li>
<li>current company <strong>with its website, headcount and industry</strong></li>
<li><strong>every past job</strong> with start and end dates</li>
<li>education with degrees and fields of study</li>
<li>skills with endorsement counts</li>
<li>certifications, recommendations, courses, honors, publications</li>
<li>connections and followers</li>
</ul>
<p>A company lookup returns firmographics: industry, headcount and range, website, HQ, founding year, type, specialties, description and followers.</p>
<p>What you <strong>won't</strong> get from a compliant provider: private emails or phone numbers scraped from profiles. Treat any vendor that promises personal contact details "from LinkedIn" with caution.</p>
<hr />
<h2>Step by step in Python</h2>
<h3>1. Get an API key</h3>
<p>Create a free account at <a href="https://enrichgrid.com/signup?ref=hashnode">enrichgrid.com</a> (50 free credits, no card) and copy the key from <strong>Dashboard → API keys</strong>. Keep it in an environment variable:</p>
<pre><code class="language-bash">export ENRICHGRID_KEY="lk_..."
pip install requests
</code></pre>
<h3>2. Enrich a person</h3>
<pre><code class="language-python">import os
import time
import requests

API = "https://enrichgrid.com/v1"
HEADERS = {"x-api-key": os.environ["ENRICHGRID_KEY"]}


def enrich_person(linkedin_url: str) -&gt; dict | None:
    """Return the profile dict, or None when the profile doesn't exist (not charged)."""
    r = requests.post(f"{API}/enrich/person", headers=HEADERS,
                      json={"identifier": linkedin_url}, timeout=60)
    r.raise_for_status()
    data = r.json()

    # Rare profiles are collected live and answer 202 with a URL to poll.
    while data["status"] == "pending":
        time.sleep(2.5)
        data = requests.get(f"https://enrichgrid.com{data['poll']}", headers=HEADERS, timeout=30).json()

    return data["profile"] if data.get("found") else None


profile = enrich_person("https://www.linkedin.com/in/williamhgates")
print(profile["first_name"], profile["last_name"], "-", profile["headline"])
print("Now:", profile["current_title"], "at", profile["current_company_name"])
</code></pre>
<p>The <code>identifier</code> accepts a full URL, the slug (<code>williamhgates</code>) or the profile ID, so you don't have to normalize URLs first.</p>
<p>The response is the same shape every time, which keeps parsing simple:</p>
<pre><code class="language-json">{
  "id": "7f3c2a9e…",
  "status": "done",
  "found": true,
  "credits_charged": 1,
  "kind": "person",
  "profile": {
    "first_name": "…",
    "headline": "…",
    "current_title": "…",
    "current_company_name": "…",
    "seniority": "cxo",
    "location": "…",
    "sections": {
      "experiences": [{"title": "…", "company_name": "…", "started_on": "2015-05-01", "ended_on": null, "is_current": true}],
      "educations": [{"school_name": "…", "degree_name": "…", "field_of_study": "…"}],
      "skills": [{"name": "…", "endorsement_count": 66}]
    }
  },
  "credits_left": 4999
}
</code></pre>
<h3>3. Walk the work history</h3>
<p>Each job in <code>sections.experiences</code> carries the employer's firmographics in <code>entreprise</code>, so you get company size and website without a second call:</p>
<pre><code class="language-python">for job in profile["sections"]["experiences"]:
    company = job.get("entreprise") or {}
    end = job["ended_on"] or "present"
    print(f'{job["title"]} @ {job["company_name"]} ({job["started_on"]} → {end})'
          f' · {company.get("staff_count_range", "?")} employees · {company.get("website_url", "")}')
</code></pre>
<h3>4. Enrich a company</h3>
<pre><code class="language-python">def enrich_company(identifier: str) -&gt; dict | None:
    r = requests.post(f"{API}/enrich/company", headers=HEADERS, json={"identifier": identifier}, timeout=60)
    r.raise_for_status()
    data = r.json()
    return data["company"] if data.get("found") else None


c = enrich_company("https://www.linkedin.com/company/microsoft")
print(c["name"], c["industry"], c["staff_count_range"], c["website_url"], c["hq_city"])
</code></pre>
<h3>5. Search people by criteria</h3>
<p>Find people by title, company, location, company size, tenure or school. Each page returns up to 10 profile slugs, which you then enrich:</p>
<pre><code class="language-python">def search_people(filters: dict, pages: int = 3) -&gt; list[str]:
    slugs, cursor = [], None
    for _ in range(pages):
        r = requests.post(f"{API}/search", headers=HEADERS,
                          json={"filters": filters, "cursor": cursor}, timeout=60)
        r.raise_for_status()
        data = r.json()
        slugs += data["slugs"]
        cursor = data["next_cursor"]
        if not cursor:
            break
    return slugs


# Heads of sales at 11–200 person companies in Germany
slugs = search_people({
    "country": ["DE"],
    "title": {"any": ["head of sales", "vp sales"], "zones": ["current"]},
    "company_size": {"min": 11, "max": 200},
})
leads = [enrich_person(s) for s in slugs]
</code></pre>
<p>Available filters: <code>country</code>, <code>city</code>, <code>title</code>, <code>company</code>, <code>past_company</code>, <code>company_size</code>, <code>company_industry_id</code>, <code>industry_id</code>, <code>school</code>, <code>career_years</code>, <code>tenure_years</code>. They combine with AND.</p>
<hr />
<h2>Migrating from Proxycurl: field mapping</h2>
<h3>Endpoints</h3>
<table>
<thead>
<tr>
<th>Proxycurl</th>
<th>EnrichGrid</th>
</tr>
</thead>
<tbody><tr>
<td><code>GET /proxycurl/api/v2/linkedin?url=…</code></td>
<td><code>POST /v1/enrich/person</code> with <code>{"identifier": url}</code></td>
</tr>
<tr>
<td><code>GET /proxycurl/api/linkedin/company?url=…</code></td>
<td><code>POST /v1/enrich/company</code></td>
</tr>
<tr>
<td><code>GET /proxycurl/api/linkedin/school?url=…</code></td>
<td><code>POST /v1/enrich/school</code></td>
</tr>
<tr>
<td>Person Search API</td>
<td><code>POST /v1/search</code></td>
</tr>
<tr>
<td><code>Authorization: Bearer &lt;key&gt;</code></td>
<td><code>x-api-key: &lt;key&gt;</code></td>
</tr>
</tbody></table>
<h3>Person fields</h3>
<table>
<thead>
<tr>
<th>Proxycurl field</th>
<th>EnrichGrid field</th>
</tr>
</thead>
<tbody><tr>
<td><code>public_identifier</code></td>
<td><code>slug</code></td>
</tr>
<tr>
<td><code>first_name</code>, <code>last_name</code></td>
<td><code>first_name</code>, <code>last_name</code></td>
</tr>
<tr>
<td><code>headline</code></td>
<td><code>headline</code></td>
</tr>
<tr>
<td><code>summary</code></td>
<td><code>summary</code></td>
</tr>
<tr>
<td><code>occupation</code></td>
<td><code>current_title</code> + <code>current_company_name</code></td>
</tr>
<tr>
<td><code>city</code>, <code>country</code></td>
<td><code>location</code>, <code>location_country</code></td>
</tr>
<tr>
<td><code>experiences[].title</code></td>
<td><code>sections.experiences[].title</code></td>
</tr>
<tr>
<td><code>experiences[].company</code></td>
<td><code>sections.experiences[].company_name</code></td>
</tr>
<tr>
<td><code>experiences[].starts_at</code> / <code>ends_at</code> (day/month/year objects)</td>
<td><code>sections.experiences[].started_on</code> / <code>ended_on</code> (ISO dates)</td>
</tr>
<tr>
<td><code>education[].school</code></td>
<td><code>sections.educations[].school_name</code></td>
</tr>
<tr>
<td><code>education[].degree_name</code>, <code>field_of_study</code></td>
<td>same names under <code>sections.educations[]</code></td>
</tr>
<tr>
<td><code>skills</code> (list of strings)</td>
<td><code>sections.skills[].name</code> (+ <code>endorsement_count</code>)</td>
</tr>
<tr>
<td><code>connections</code>, <code>follower_count</code></td>
<td><code>num_connections</code>, <code>follower_count</code></td>
</tr>
</tbody></table>
<p>A small adapter keeps the rest of your code untouched:</p>
<pre><code class="language-python">def to_proxycurl_shape(p: dict) -&gt; dict:
    """Map an EnrichGrid profile to the Proxycurl fields most code reads."""
    s = p.get("sections", {})
    def ymd(d):
        if not d:
            return None
        y, m, dd = (int(x) for x in d.split("-"))
        return {"year": y, "month": m, "day": dd}
    return {
        "public_identifier": p.get("slug"),
        "first_name": p.get("first_name"),
        "last_name": p.get("last_name"),
        "full_name": " ".join(filter(None, [p.get("first_name"), p.get("last_name")])),
        "headline": p.get("headline"),
        "summary": p.get("summary"),
        "occupation": " at ".join(filter(None, [p.get("current_title"), p.get("current_company_name")])),
        "country": p.get("location_country"),
        "connections": p.get("num_connections"),
        "follower_count": p.get("follower_count"),
        "experiences": [{"title": e.get("title"), "company": e.get("company_name"),
                         "starts_at": ymd(e.get("started_on")), "ends_at": ymd(e.get("ended_on"))}
                        for e in s.get("experiences", [])],
        "education": [{"school": e.get("school_name"), "degree_name": e.get("degree_name"),
                       "field_of_study": e.get("field_of_study")} for e in s.get("educations", [])],
        "skills": [k["name"] for k in s.get("skills", [])],
    }
</code></pre>
<hr />
<h2>Enriching thousands of URLs</h2>
<p>For lists, run requests in parallel up to your plan's rate limit and respect <code>Retry-After</code> on <code>429</code>:</p>
<pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor
import csv

session = requests.Session()
session.headers.update(HEADERS)


def enrich_safe(url: str, attempts: int = 5) -&gt; dict:
    for _ in range(attempts):
        r = session.post(f"{API}/enrich/person", json={"identifier": url}, timeout=90)
        if r.status_code == 429 or r.status_code == 503:   # rate limit or busy: nothing charged
            time.sleep(float(r.headers.get("Retry-After", 2)))
            continue
        if r.status_code == 402:
            raise RuntimeError("Out of credits")
        r.raise_for_status()
        data = r.json()
        while data["status"] == "pending":
            time.sleep(2.5)
            data = session.get(f"https://enrichgrid.com{data['poll']}", timeout=30).json()
        return {"url": url, "found": data.get("found"), **(data.get("profile") or {})}
    return {"url": url, "found": None}


urls = [line.strip() for line in open("linkedin_urls.txt") if line.strip()]
with ThreadPoolExecutor(max_workers=5) as pool:   # match your plan's requests/second
    rows = list(pool.map(enrich_safe, urls))

with open("enriched.csv", "w", newline="", encoding="utf-8") as f:
    fields = ["url", "found", "first_name", "last_name", "headline", "current_title",
              "current_company_name", "seniority", "location", "location_country"]
    w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
    w.writeheader()
    w.writerows(rows)
</code></pre>
<p>A few practical notes:</p>
<ul>
<li><strong>Not-found profiles are free</strong>, so you don't need to pre-clean the list.</li>
<li><strong>Repeat lookups within 30 days come from cache</strong>, instantly, so re-running a job after a crash is cheap and fast.</li>
<li>If you'd rather not write code, upload an <strong>Excel or CSV</strong> file in the dashboard (<a href="https://enrichgrid.com/bulk-linkedin-enrichment?ref=hashnode">bulk enrichment</a>) and download it with every field filled in.</li>
</ul>
<h3>Using it from an AI agent</h3>
<p>The same data is available as an MCP server for Claude, Cursor and other agents. In Claude Code:</p>
<pre><code class="language-bash">claude mcp add --transport http enrichgrid https://enrichgrid.com/mcp --header "x-api-key: $ENRICHGRID_KEY"
</code></pre>
<p>Then ask the agent things like "enrich these 20 LinkedIn URLs and rank them by seniority".</p>
<hr />
<h2>FAQ</h2>
<p><strong>Is there an official LinkedIn API for profile data?</strong> Not for general enrichment. LinkedIn's APIs give access to the signed-in member's own data, and broader access goes through partner programs that are rarely granted for this use case.</p>
<p><strong>Is it legal to use LinkedIn profile data?</strong> Public professional data is widely used for B2B sales, recruiting and research, but you are responsible for how you use it. If you process data about people in the EU, you need a lawful basis under GDPR (often legitimate interest for B2B), and you must honor opt-out and deletion requests. Don't use it for spam.</p>
<p><strong>How fresh is the data?</strong> EnrichGrid collects profiles live and caches them for up to 30 days. You can force a fresh collection with <code>"live": true</code> at the same price.</p>
<p><strong>Do I get emails or phone numbers?</strong> No. The API returns professional profile data only.</p>
<p><strong>How much does it cost?</strong> One credit per profile or company found; lookups that find nothing are free. 50 free credits on sign-up, then plans from $29/month for 10,000 credits, down to about $0.001 per profile at volume. See <a href="https://enrichgrid.com/?ref=hashnode#pricing">pricing</a>.</p>
<p><strong>What are the rate limits?</strong> From 2 requests/second on the free tier to 50 on Enterprise. Responses include <code>X-RateLimit-*</code> headers, and <code>429</code> responses include <code>Retry-After</code>.</p>
<hr />
<h2>Wrapping up</h2>
<p>Replacing Proxycurl is mostly three changes: a new endpoint and auth header, a field-mapping adapter, and polling for the rare profile that's collected live. The rest of your pipeline can stay as it is.</p>
<ul>
<li>📘 API reference: <a href="https://enrichgrid.com/api-docs?ref=hashnode">enrichgrid.com/api-docs</a></li>
<li>🔁 Migration guide: <a href="https://enrichgrid.com/proxycurl-alternative?ref=hashnode">enrichgrid.com/proxycurl-alternative</a></li>
<li>🐍 Python guide: <a href="https://enrichgrid.com/guides/linkedin-data-python?ref=hashnode">enrichgrid.com/guides/linkedin-data-python</a></li>
</ul>
<p>If you migrated from Proxycurl to something else, I'd like to hear what worked for you in the comments.</p>
<p><em>Originally published on</em> <a href="https://dev.to/enrichgrid/proxycurl-alternative-in-2026-how-to-get-linkedin-profile-and-company-data-with-an-api-python-1nkg"><em>dev.to</em></a><em>.</em></p>
]]></content:encoded></item></channel></rss>