Migrating from Proxycurl to EnvoAPI, endpoint by endpoint
A working migration guide: how the URL-keyed calls map, what the response envelope changes, the adapter that keeps your internal contracts intact, and how to cut over without a live provider to compare against.

LinkedIn sued Proxycurl in January 2025 over scraping and a network of fake accounts. Proxycurl shut down in July 2025, and the case was resolved with a permanent injunction requiring it to stop accessing LinkedIn and delete the data it had collected. Any pipeline still calling those endpoints is already failing. This guide maps the calls one by one, shows what actually changes in the response, and gives a cutover plan that works now that there is no live provider left to compare against.
What actually changes
Most of the migration is mechanical. Proxycurl was keyed on LinkedIn URLs and returned the record at the top level under snake_case field names; EnvoAPI takes the same URL on its detail endpoints, wraps every response in one envelope, and names fields in camelCase. Nothing about your downstream data model has to change if you put an adapter in the right place.
| Aspect | Proxycurl | EnvoAPI |
|---|---|---|
| Authentication | Authorization: Bearer <key> | X-API-Key: <key> |
| Primary key for lookups | Full LinkedIn URL | Full URL on /detail endpoints, or the public slug on the path |
| Response shape | Record at the top level | success, data, pagination, meta envelope |
| Field naming | snake_case | camelCase |
| Errors | Provider-specific bodies | meta.error with statusCode, type, and detail |
| Rate limiting | Credit model with monthly minimums | 429 with type: rate_limited, never queued |
Endpoint mapping
The /detail endpoints exist precisely for this shape of call: they take the same url parameter your Proxycurl code already passes, so the majority of call sites change only a base URL, a header, and a path.
| Proxycurl call | EnvoAPI endpoint | Note |
|---|---|---|
| Person profile by URL | GET /api/v2/profiles/detail?url= | Same input, same idea |
| Person profile by slug | GET /api/v2/profiles/{public_identifier} | Slug only — do not pass a full URL here |
| Company profile by URL | GET /api/v2/companies/detail?url= | Company overview, industry, headcount |
| Company lookup by domain | GET /api/v2/companies/by-domain?domain= | Paginated with offset and limit |
| Employee listing | GET /api/v2/companies/{universal_name}/employees | Slug from the company record |
| Job detail | GET /api/v2/jobs/detail?url= | Posting metadata and company linkage |
| Person search | GET /api/v2/search/people | Filters come from /api/v2/search/filters |
| Job search | GET /api/v2/jobs/search | Filters come from /api/v2/jobs/search/filters |
The full surface is 47 documented endpoints across profiles, companies, search, jobs, and posts. If a Proxycurl call has no single equivalent, it usually composes from two — a company lookup followed by an employee listing, for example.
Changing the request
Authentication moves from a bearer token to the X-API-Key header. Keep the key in the environment, on the server, and out of anything that reaches a browser bundle.
import osimport requests# Before — Proxycurl, keyed on the profile URLrequests.get( "https://nubela.co/proxycurl/api/v2/linkedin", headers={"Authorization": f"Bearer {os.environ['PROXYCURL_KEY']}"}, params={"url": profile_url}, timeout=20,)# After — EnvoAPI, same input, different header and pathrequests.get( "https://api.envoapi.com/api/v2/profiles/detail", headers={"X-API-Key": os.environ["ENVO_API_KEY"]}, params={"url": profile_url}, timeout=20,)Unwrapping the envelope and keeping your contracts
This is where migrations actually go wrong — not on paths, but on the shape underneath them. Every EnvoAPI response carries success, data, pagination, and meta, so the record your code wants is one level down, and failures arrive as meta.error rather than as an exception from the HTTP client. One adapter function at the edge of your system keeps that detail out of every service that consumes it.
| Proxycurl field | EnvoAPI field |
|---|---|
first_name | firstName |
last_name | lastName |
full_name | fullName |
headline | headline |
public_identifier | publicIdentifier |
follower_count | followerCount |
experiences[].company | currentPositions[].companyName |
experiences[].title | currentPositions[].title |
class EnrichmentError(Exception): passdef to_internal_profile(payload: dict) -> dict: """Unwrap one EnvoAPI response into the shape our services expect.""" if not payload.get("success"): error = (payload.get("meta") or {}).get("error", {}) raise EnrichmentError(error.get("type", "unknown"), error.get("detail")) data = payload["data"] current = (data.get("currentPositions") or [None])[0] or {} return { "first_name": data.get("firstName"), "last_name": data.get("lastName"), "headline": data.get("headline"), "public_identifier": data.get("publicIdentifier"), "current_company": current.get("companyName"), "current_title": current.get("title"), }A missing field is a state, not a failure. Public data is uneven: treat a null headline or an empty position list as a value your workflow handles, the way it already handles an unresolved record.
Errors and rate limits
Rate limits return 429 with type: rate_limited in meta.error — requests are rejected rather than queued, so the client owns the backoff. Bounded exponential backoff over a few attempts is enough; retry loops without a ceiling turn one slow minute into an outage. The rate limits and errors pages document the response bodies and every error type.
Rolling out without a live provider to compare against
Standard migration advice says run both providers in parallel and diff the results. That is not available here: Proxycurl has been shut down since July 2025, so there is nothing live to compare against. Use what you already stored instead.
- Audit the call sites first — endpoint, monthly volume, and the fields your business logic actually reads. Fields nobody reads do not need mapping.
- Take a sample of records your pipeline enriched through Proxycurl and re-enrich them through EnvoAPI, then diff coverage field by field. Your own historical output is the baseline the dead provider can no longer give you.
- Test the whole thing on the free plan first — it includes 100 credits, which is enough to check coverage on a real sample before any commitment.
- Put the new client behind a feature flag and move traffic in stages, highest-value flows first, so a coverage surprise affects one workflow rather than all of them.
- Watch error rates, latency, and enrichment coverage for a few days per stage, then remove the Proxycurl code paths once the last flag is on.
Prioritise by blast radius: real-time enrichment on a user-facing screen before nightly exports, and anything feeding a customer-visible number before internal dashboards.
What to read while you migrate
- Authentication — where the key goes and how to keep it server-side.
- Rate limits — the
429body and a bounded backoff example. - Errors — every error type and what to do about it.
- Pagination — cursors and offsets for list endpoints such as employee listings and search.
- Contact the team if a Proxycurl call in your inventory has no obvious equivalent — the composition is usually two endpoints, and it is worth confirming before you build around a gap.
FAQs about the migration
Is EnvoAPI a drop-in replacement for Proxycurl?
Close, but not byte-for-byte. The URL-keyed detail endpoints take the same input your existing code passes, so the request change is small. The response differs in two ways that matter: it arrives inside a success/data/pagination/meta envelope, and fields are camelCase rather than snake_case. An adapter absorbs both in one place, and you can drop it later if you decide to adopt the native schema throughout.
How long does the migration take?
It scales with the number of call sites and downstream consumers, not with the API. The adapter and the request changes are usually a short job; the long pole is verifying coverage on your own records and staging the rollout behind flags. A single service with a handful of endpoints is quick. A dozen services that each parse Proxycurl-shaped records directly will spend most of the time on the parsing, which is the argument for adding the adapter first.
Can I run Proxycurl and EnvoAPI side by side to compare?
No — Proxycurl stopped serving traffic in July 2025, so there is no live provider to diff against. Compare against the records you already enriched and stored instead: re-enrich a representative sample and check coverage per field. If you are moving from a provider that is still operating, a parallel run is still the better test.
What if a Proxycurl endpoint has no exact EnvoAPI equivalent?
Compose it. A company lookup followed by an employee listing covers most people-at-company workflows, and job detail plus job search covers most feed-shaped ones. Where a composition changes the shape, absorb the difference in the adapter so your internal contract is unchanged. If a gap blocks a critical flow, send the details through the contact form rather than designing around it blind.