Proxycurl shut down in July 2025. This guide maps its endpoints and fields to EnrichGrid, with a Python adapter that returns the Proxycurl-style keys your code already reads, a Node.js example, and the differences you need to handle.
POST with a JSON body {"identifier": "…"} (a LinkedIn URL, slug or ID) instead of GET with query parameters.x-api-key: YOUR_KEY instead of Authorization: Bearer.{"found": true, "profile": {…}} (companies use "company", schools "school"). A profile that doesn't exist returns 200 with "found": false and costs nothing.202 with "status": "pending" and a poll URL. Poll until the status is done or failed; polling is free."2015-05-01") instead of {day, month, year} objects, and the country is an ISO code ("US")."live": true collects fresh at the same price.| What you used | Proxycurl | EnrichGrid |
|---|---|---|
| Person profile | GET /proxycurl/api/v2/linkedin?url=… | POST /v1/enrich/person |
| Company profile | GET /proxycurl/api/linkedin/company?url=… | POST /v1/enrich/company |
| School profile | GET /proxycurl/api/linkedin/school?url=… | POST /v1/enrich/school |
| Person search | GET /proxycurl/api/v2/search/person?… | POST /v1/search |
| Slow results | – | GET /v1/lookups/{id} (free) |
| Auth | Authorization: Bearer … | x-api-key: … |
| Proxycurl | EnrichGrid (profile.…) |
|---|---|
public_identifier | slug |
first_name, last_name, full_name | first_name, last_name (join them for the full name) |
headline, summary | headline, summary |
occupation | current_title + current_company_name |
country, city | location_country (ISO code), location (free text) |
profile_pic_url | photo_url |
connections, follower_count | num_connections, follower_count |
experiences[] | sections.experiences[]: title, company_name, started_on, ended_on, is_current, plus entreprise with the employer's firmographics |
education[] | sections.educations[]: school_name, degree_name, field_of_study, started_on, ended_on |
skills[] | sections.skills[].name (with endorsement_count) |
certifications[] | certifications[] |
| (not in Proxycurl) | seniority, job_function, current_companies[] |
| Proxycurl | EnrichGrid (company.…) |
|---|---|
name, description, tagline | name, description, tagline |
website | website_url |
industry | industry |
company_size [min, max] | staff_count_range (e.g. "201-500") |
company_size_on_linkedin | staff_count |
hq {city, state, country} | hq_city, hq_region, country_code |
founded_year, follower_count | founded_year, follower_count |
specialities | specialties |
locations[] | locations[] |
If your code reads Proxycurl fields in many places, convert the EnrichGrid response once, at the edge, and leave the rest of your code alone.
Ready-made adapter: download enrichgrid_proxycurl.py (one file, MIT, needs only requests) or install it from GitHub with pip install "git+https://github.com/enrichgrid/proxycurl-adapter". It handles polling and retries, and client.person(url) / client.company(url) return Proxycurl-style dicts, or None when nothing is found (no charge). It's tested on synthetic fixtures and on the sample responses in our API docs. The same mapping, inline:
import os, time, requests
API = "https://enrichgrid.com/v1"
H = {"x-api-key": os.environ["ENRICHGRID_KEY"]}
def enrich(kind, identifier, max_wait=300):
"""kind: "person", "company" or "school". Returns the finished lookup."""
r = requests.post(f"{API}/enrich/{kind}", headers=H, json={"identifier": identifier}, timeout=120)
r.raise_for_status() # 401 bad key, 402 no credits, 429 slow down (see Retry-After)
data = r.json()
poll, deadline = data.get("poll"), time.time() + max_wait
while data.get("status") == "pending" and time.time() < deadline: # 202: slow profile, polling is free
time.sleep(3)
data = requests.get("https://enrichgrid.com" + poll, headers=H, timeout=30).json()
return data
def _date(iso):
# "2015-05-01" -> {"day": 1, "month": 5, "year": 2015}
# When only the year is known the date is YYYY-01-01; started_on_display shows the real precision.
if not iso:
return None
year, month, day = (int(x) for x in iso.split("-"))
return {"day": day, "month": month, "year": year}
def to_proxycurl_person(p):
"""EnrichGrid profile -> the Proxycurl person fields most integrations read."""
sections = p.get("sections") or {}
return {
"public_identifier": p.get("slug"),
"first_name": p.get("first_name"),
"last_name": p.get("last_name"),
"full_name": " ".join(x for x in (p.get("first_name"), p.get("last_name")) if x),
"headline": p.get("headline"),
"summary": p.get("summary"),
"occupation": " at ".join(x for x in (p.get("current_title"), p.get("current_company_name")) if x),
"country": p.get("location_country"), # ISO code, e.g. "US"
"city": p.get("location"), # free text, e.g. "San Francisco Bay Area"
"profile_pic_url": p.get("photo_url"),
"connections": p.get("num_connections"),
"follower_count": p.get("follower_count"),
"experiences": [{
"title": e.get("title"),
"company": e.get("company_name"),
"company_linkedin_profile_url": (
f"https://www.linkedin.com/company/{e['entreprise']['slug']}/"
if (e.get("entreprise") or {}).get("slug") else None),
"description": e.get("description"),
"location": e.get("location"),
"starts_at": _date(e.get("started_on")),
"ends_at": _date(e.get("ended_on")),
} for e in sections.get("experiences") or []],
"education": [{
"school": ed.get("school_name"),
"degree_name": ed.get("degree_name"),
"field_of_study": ed.get("field_of_study"),
"starts_at": _date(ed.get("started_on")),
"ends_at": _date(ed.get("ended_on")),
} for ed in sections.get("educations") or []],
"skills": [s.get("name") for s in sections.get("skills") or []],
"certifications": p.get("certifications") or [],
}
def to_proxycurl_company(c):
"""EnrichGrid company -> the Proxycurl company fields most integrations read."""
low, _, high = (c.get("staff_count_range") or "").replace("+", "-").partition("-")
return {
"name": c.get("name"),
"description": c.get("description"),
"website": c.get("website_url"),
"industry": c.get("industry"),
"company_size": [int(low) if low.isdigit() else None, int(high) if high.isdigit() else None],
"company_size_on_linkedin": c.get("staff_count"),
"founded_year": c.get("founded_year"),
"specialities": c.get("specialties") or [],
"follower_count": c.get("follower_count"),
"tagline": c.get("tagline"),
"hq": {"city": c.get("hq_city"), "state": c.get("hq_region"), "country": c.get("country_code")},
}
data = enrich("person", "https://www.linkedin.com/in/brendaneich")
if data.get("found"):
person = to_proxycurl_person(data["profile"])
print(person["full_name"], "-", person["occupation"])// Node.js 18+ (built-in fetch), saved as an ES module, e.g. enrich.mjs
const API = "https://enrichgrid.com/v1";
const H = { "x-api-key": process.env.ENRICHGRID_KEY, "content-type": "application/json" };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function enrich(kind, identifier, maxWaitMs = 300000) {
const res = await fetch(`${API}/enrich/${kind}`, { method: "POST", headers: H, body: JSON.stringify({ identifier }) });
if (!res.ok) throw new Error(`EnrichGrid ${res.status}: ${await res.text()}`);
let data = await res.json();
const poll = data.poll;
const deadline = Date.now() + maxWaitMs;
while (data.status === "pending" && Date.now() < deadline) { // 202: slow profile, polling is free
await sleep(3000);
data = await (await fetch("https://enrichgrid.com" + poll, { headers: H })).json();
}
return data;
}
const data = await enrich("person", "https://www.linkedin.com/in/brendaneich");
if (data.found) console.log(data.profile.first_name, data.profile.last_name, "-", data.profile.current_title);Proxycurl's person search took query parameters. EnrichGrid takes a filters object and returns up to 10 profile slugs per page, which you then enrich:
requests.post(f"{API}/search", headers=H, json={"filters": {
"country": ["US"], "title": {"any": ["vp sales"]}, "company": ["microsoft"]}})Filters: country, city, title, company (current), past_company, company_size, company_industry_id, industry_id, school, career_years, tenure_years. Page with cursor / next_cursor. 1 credit per page with results; empty pages are free. Details in the API reference.
company filter (10 people per page).Create a free account (50 credits, no card), run the last 50–100 LinkedIn URLs from your logs through the adapter and compare the fields you use. Not-found lookups are free, so stale URLs cost nothing. For a whole file, upload it to Bulk enrich.
No. Endpoints, auth and the response shape differ. The inputs are the same (a LinkedIn URL, slug or ID), so most integrations need only the small adapter above or a direct field mapping.
No. Not-found lookups and empty search pages are free.
Collected live on the first request and kept for up to 30 days. Send "live": true to collect fresh at the same price.
No. EnrichGrid returns professional profile and company data only.
50 free credits. No credit card. Pay only for what we find.
Create a free account →