Proxycurl Field Mapping Cheatsheet for 2026
Proxycurl shut down in July 2025 after LinkedIn's federal lawsuit landed. Nine months later, the old schema is still wired into production codebases everywhere. This is the field-by-field translation so you can swap providers without rewriting your data layer. LinkFetch, a compliance-first LinkedIn data API, gets the most coverage here, but the patterns apply to every serious alternative.
Why field mapping is the hidden cost of a Proxycurl migration
Most "Proxycurl alternatives" posts focus on the wrong thing. They rank
vendors on coverage and pricing, then hand-wave the integration step
with "just swap the endpoint". That is the part that actually breaks.
LinkFetch's /v1/profiles returns camelCase keys, nested arrays for
employment, and ISO-8601 dates. Proxycurl returned snake_case, flat
arrays with denormalised company stubs, and partial dates with null
days. A direct find-and-replace will compile and silently corrupt
roughly 18% of records on the first run.
The cost is not the API contract. It is the assumptions baked into your
ETL: the current_company field your CRM writes to, the experiences[*]. company.name your enrichment scoring reads, the timezone-naive joined_ on date your "tenure" calculation depends on. Every one of those is a
rename in your code, not in theirs.
A second hidden cost is null shape. Proxycurl returned null for
missing fields. Modern providers vary: LinkedIn's own People Search API
returns the field omitted entirely, LinkdAPI uses an empty string, and
LinkFetch returns null to match Proxycurl's behaviour deliberately. If
your downstream code does if (record.field !== null), that test breaks
in two of three cases. Map it once at the adapter and never again
downstream.
The full mapping table
The table below covers the eighteen fields that 90% of Proxycurl integrations actually used. The "LinkFetch" column shows the canonical path in the LinkFetch v1 response. The "Notes" column flags the gotchas covered in detail in the next section.
| Proxycurl field | LinkFetch v1 | Notes |
|---|---|---|
public_identifier |
publicIdentifier |
direct rename |
full_name |
fullName |
direct rename |
first_name |
firstName |
direct rename |
last_name |
lastName |
direct rename |
headline |
headline |
unchanged |
summary |
summary |
unchanged |
country_full_name |
location.country |
nested, ISO-3166 alpha-2 also at location.countryCode |
city |
location.city |
nested under location |
state |
location.region |
renamed to "region" to handle non-US |
occupation |
derived from experience[0] |
not a stored field; compute at read |
experiences |
experience |
array name singularised |
experiences[*].company |
experience[*].company.name |
nested under company object |
experiences[*].company_linkedin_profile_url |
experience[*].company.linkedinUrl |
renamed |
experiences[*].starts_at |
experience[*].startDate |
ISO-8601 string, not partial |
experiences[*].ends_at |
experience[*].endDate |
null if current |
education |
education |
unchanged |
connections |
connectionCount |
renamed; also typed as integer not string |
follower_count |
followerCount |
direct rename |
Eighteen fields covers most of the surface, but the long tail matters.
Profile-level fields like accomplishment_courses, volunteer_work,
and recommendations exist on LinkFetch under accomplishments.courses,
accomplishments.volunteer, and recommendations respectively, all
nested under an accomplishments parent that Proxycurl flattened.
Five fields that don't translate cleanly
These are the ones taht bite people on day three of the migration, after the easy renames pass code review and the integration looks green.
1. occupation. Proxycurl synthesised this field by concatenating
the most recent experience's title and company. LinkFetch does not
store it. You compute it at read time from experience[0].title and
experience[0].company.name. If experience is empty (rare, but real
for stub profiles), fall back to the headline.
2. Date precision. Proxycurl returned partial dates as
{"day": null, "month": 5, "year": 2022}. LinkFetch returns ISO-8601
strings. When the source only knows month and year, LinkFetch returns
"2022-05-01", choosing the first of the month deterministically.
Code that did if (date.day === null) to detect imprecision needs a
new signal: LinkFetch exposes experience[*].startDatePrecision as
"day", "month", or "year" so you can preserve the distinction.
3. Currently-employed flag. Proxycurl had no boolean for "is this
person currently at this company". You inferred it from
ends_at === null. LinkFetch keeps that pattern (endDate === null
means current) and also exposes experience[*].current as a derived
boolean. Use whichever your code already reads, but pick one.
4. Company stubs vs. company objects. Proxycurl inlined a
denormalised company string with the URL. LinkFetch returns a small
nested company object: {name, linkedinUrl, industry, employeeCount}.
Industry and headcount were never on the Proxycurl experience entry,
so this is genuinely new data. If you join experiences against a
companies table downstream, you can drop that join for fields that
LinkFetch already inlines.
5. The connections integer-vs-string thing. Proxycurl returned
connections as a string, sometimes "500+", sometimes "342". LinkFetch
returns connectionCount as a true integer. The "500+" case is now
represented as connectionCount: 500 plus connectionCountIsCapped: true. Anyone parsing the string with a regex needs to delete that
code and read two fields instead.
Building the adapter pattern
The right shape for the migration is an adapter, not a search-and- replace. One file, one function, mapped tests. Here is the skeleton most teams converge on:
// adapters/profile.ts
import type { LinkFetchProfile } from "@linkfetch/sdk";
export type ProxycurlShape = {
public_identifier: string;
full_name: string;
occupation: string;
experiences: Array<{
company: string;
title: string;
starts_at: { day: number | null; month: number; year: number };
ends_at: { day: number | null; month: number; year: number } | null;
}>;
// ... other Proxycurl fields
};
export function toLegacyShape(p: LinkFetchProfile): ProxycurlShape {
return {
public_identifier: p.publicIdentifier,
full_name: p.fullName,
occupation: p.experience[0]
? `${p.experience[0].title} at ${p.experience[0].company.name}`
: p.headline,
experiences: p.experience.map((e) => ({
company: e.company.name,
title: e.title,
starts_at: isoToPartial(e.startDate, e.startDatePrecision),
ends_at: e.endDate
? isoToPartial(e.endDate, e.endDatePrecision)
: null,
})),
};
}
Two things to flag here. First, the adapter goes in your codebase, not LinkFetch's. Vendor-supplied compatibility shims are tempting and they rot fast. Second, the adapter is a one-way door: write code against the new shape going forward and let the legacy adapter only feed the parts of your system you have not yet migrated. Otherwise you end up maintaining the Proxycurl schema forever.
How LinkFetch handles the gotchas Proxycurl didn't
Two structural differences worth calling out, since they are not just renames.
Date imprecision is explicit. LinkFetch returns
startDatePrecision and endDatePrecision alongside the date itself.
You can preserve "month-only" vs. "year-only" knowledge through your
ETL without parsing the date string for telltale -01 suffixes.
Caching is per-user, not global. LinkFetch charges full credits on every call by default and does not silently de-duplicate behind the scenes. If you want a cache, you build it. The trade-off is that data freshness is yours to decide; there is no shared global cache that quietly returns 3-month-old records when you asked for current state. For migrators coming from Proxycurl's freshness ambiguity, this is a welcome change.
The compliance posture is the other structural shift. Where Proxycurl operated as a third-party scraper, LinkFetch runs inside the user's own LinkedIn session via a Chrome extension. The user is the principal. That changes what you can legally do with the data far more than it changes the shape of the response, but it is worth knowing the architecture before you commit. The GDPR posture of the surviving providers covers the legal half in depth.
Frequently asked questions
Does LinkFetch publish a Proxycurl-compatible endpoint?
No, and we deliberately chose not to. Compatibility shims encourage half-migrations, where the team renames imports and never updates the ETL. The adapter pattern in this post is the supported path: write the shim in your codebase, against your tests, and migrate fully on your own schedule.
What about the lookup_email endpoint?
Proxycurl's lookup_email endpoint returned a profile from an email
address. LinkFetch does not offer a direct equivalent for compliance
reasons; email reverse-lookup operates outside the user's own
LinkedIn session and falls outside our user-as-principal model.
Reverse-lookup providers still exist on the market, but the legal
posture is meaningfully different. This is one of the few cases where
"map field by field" does not apply and you need to rework the
workflow.
How long does the migration take in practice?
The teams who finished it inside one engineering week followed three rules: write the adapter first with tests against canned Proxycurl fixtures, swap the data source second, deprecate the adapter third (over the next quarter, as downstream code moves to the new shape). The teams who took six weeks tried to map field-by-field at every call site at once. Don't do that.
What about historical Proxycurl data already in our warehouse?
Leave it. Run new fetches through LinkFetch in the new shape, and either run the adapter in reverse for short-term consistency or project both shapes through a view layer that normalises at read time. Backfilling existing rows is rarely worth it; the data is stale anyway, and the next refresh solves it for free.
Last updated: 2026-04-27 by the LinkFetch team.