Enrich Your Sales CRM with LinkedIn Employee Data (2026)
Pull structured LinkedIn employee lists into HubSpot, Salesforce, or any CRM. Step-by-step enrichment pipeline with title, location, and headline fields.

Thirdwatch's LinkedIn Company Employees Scraper pulls structured employee lists — name, headline, title, location, profile URL — and pushes them into your CRM as enriched contacts. No LinkedIn login required. Built for B2B sales teams, RevOps engineers, and growth teams who need accurate account-level contact data without a per-seat subscription.
Why enrich your CRM with LinkedIn employee data
CRM contact decay is the silent killer of outbound sales pipelines. According to LinkedIn's 2024 B2B Marketing Benchmark, 30% of B2B professional contact data goes stale every year as people change roles, leave companies, or update titles. For a 10,000-contact CRM, that is 3,000 bad records per year — bad contacts that waste SDR time, inflate bounce rates, and depress outbound conversion.
LinkedIn is the most current source of professional contact data for B2B sales. Employees update their LinkedIn profiles when they change roles — often before their company website or a third-party data vendor catches the change. An enrichment pipeline that pulls directly from LinkedIn at run time guarantees freshness that no licensed data vendor can match without a comparable cadence.
The job-to-be-done is straightforward. An SDR team keeps a list of 200 target accounts in Salesforce. They need contacts — with current titles and LinkedIn URLs — for each account's buying committee. A RevOps engineer maintains a HubSpot account of 5,000 existing customers and needs to refresh contact records whenever an account goes through a renewal cycle. A growth team at a B2B SaaS company wants to pre-populate an outbound sequence tool with enriched contacts before a campaign launches. All three cases reduce to the same pipeline: account list in, enriched contact records out.
How does this compare to the alternatives?
Three common approaches to CRM contact enrichment:
| Approach | Cost per 1,000 contacts | Reliability | Setup time | Maintenance |
|---|---|---|---|---|
| ZoomInfo / Apollo.io subscription | Subscription required | High, includes email + phone | Hours | Annual contract |
| LinkedIn Sales Navigator (manual export) | Per seat, manual process | High fidelity, rate-limited | Continuous sourcer time | No automation |
| Thirdwatch LinkedIn Company Employees Scraper | Pay per result | Production-tested, no login | 5 minutes | Thirdwatch tracks LinkedIn changes |
ZoomInfo and Apollo include email addresses and direct dials, which Thirdwatch does not. The LinkedIn Company Employees Scraper actor page covers the enrichment fields available on LinkedIn's public surface. For teams that need email addresses, the recommended pattern is to enrich LinkedIn data first, then pass profile URLs to a targeted email-finding service on the smaller shortlist rather than paying for full-database access across all contacts.
How to enrich a CRM with LinkedIn employee data in 5 steps
Step 1: How do I get my Apify API token?
Sign in at apify.com (free trial, no credit card required), navigate to Settings → Integrations, and copy your personal API token:
export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"Step 2: How do I pull employees for a list of target accounts?
Pass company names or LinkedIn company URLs and receive structured employee records.
import os
import requests
ACTOR = "thirdwatch~linkedin-company-employees-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
# Use LinkedIn company URLs for precision, names for convenience
TARGET_ACCOUNTS = [
"https://www.linkedin.com/company/stripe/",
"https://www.linkedin.com/company/anthropic/",
"https://www.linkedin.com/company/datadog/",
"Rippling",
"Deel",
]
resp = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"queries": TARGET_ACCOUNTS,
"mode": "basic",
"maxResults": 100,
},
timeout=3600,
)
employees = resp.json()
print(f"Fetched {len(employees)} employee records")mode: "basic" returns name, headline, URL, current position, and source company — the minimum useful set for CRM contact creation.
Step 3: How do I filter to the right buyer personas?
Apply a job title filter to narrow to decision-makers before enriching.
import re
# Alternatively, pass jobTitle to the actor to filter at source —
# more efficient for large companies.
DM_PATTERN = re.compile(
r"\b(VP|Vice President|Director|Head of|Chief|C[EFT]O|"
r"Senior Director|Principal|General Manager)\b",
re.IGNORECASE,
)
decision_makers = [
emp for emp in employees
if DM_PATTERN.search(emp.get("headline") or "")
]
print(f"{len(decision_makers)} decision-makers from "
f"{len(set(e.get('sourceCompany','') for e in employees))} accounts")For cleaner results, pass jobTitle directly in the actor input — filtering at source reduces the result count and lowers enrichment cost.
Step 4: How do I upsert contacts into HubSpot?
Map the employee record fields to HubSpot's Contact schema and upsert by LinkedIn URL.
import os
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HS_BASE = "https://api.hubspot.com/crm/v3/objects/contacts"
def upsert_contact(emp: dict) -> dict:
headline = emp.get("headline") or ""
# Split headline "Title at Company" into parts
title_part = headline.split(" at ")[0] if " at " in headline else headline
payload = {
"properties": {
"firstname": (emp.get("fullName") or "").split(" ")[0],
"lastname": " ".join((emp.get("fullName") or "").split(" ")[1:]),
"jobtitle": title_part,
"company": emp.get("sourceCompany", ""),
"city": emp.get("location", ""),
"hs_linkedin_url": emp.get("url", ""),
"lifecyclestage": "lead",
"lead_source": "LinkedIn Employee Enrichment",
}
}
# Upsert by linkedin URL to avoid duplicates
search_resp = requests.post(
f"{HS_BASE}/search",
headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
json={
"filterGroups": [{
"filters": [{
"propertyName": "hs_linkedin_url",
"operator": "EQ",
"value": emp.get("url", ""),
}]
}]
},
timeout=10,
)
existing = search_resp.json().get("results", [])
if existing:
contact_id = existing[0]["id"]
requests.patch(
f"{HS_BASE}/{contact_id}",
headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
json=payload,
timeout=10,
)
return {"action": "updated", "id": contact_id}
else:
create_resp = requests.post(
f"{HS_BASE}",
headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
json=payload,
timeout=10,
)
return {"action": "created", "id": create_resp.json().get("id")}
results = [upsert_contact(emp) for emp in decision_makers]
created = sum(1 for r in results if r.get("action") == "created")
updated = sum(1 for r in results if r.get("action") == "updated")
print(f"CRM enrichment complete — {created} created, {updated} updated")Step 5: How do I schedule recurring enrichment runs?
Use Apify's schedule API to re-enrich accounts on a cadence matching your sales cycle.
import os, requests
TOKEN = os.environ["APIFY_TOKEN"]
# Create a weekly schedule — runs every Monday at 7 AM UTC
schedule_payload = {
"name": "Weekly LinkedIn CRM Enrichment",
"cronExpression": "0 7 * * 1",
"isEnabled": True,
"actions": [
{
"type": "RUN_ACTOR",
"actorId": "thirdwatch~linkedin-company-employees-scraper",
"input": {
"queries": [
"https://www.linkedin.com/company/stripe/",
"https://www.linkedin.com/company/datadog/",
],
"mode": "basic",
"maxResults": 100,
},
}
],
}
resp = requests.post(
"https://api.apify.com/v2/schedules",
params={"token": TOKEN},
json=schedule_payload,
timeout=30,
)
print(f"Schedule created: {resp.json().get('id')}")Each scheduled run pushes fresh records into the dataset. Wire a webhook from the run-finish event to trigger your CRM upsert script automatically.
Sample output
A single employee record in Basic mode looks like this. Twenty records of this shape weigh roughly 8 KB.
{
"fullName": "Priya Mehta",
"headline": "VP of Sales at Stripe",
"url": "https://www.linkedin.com/in/priyamehta/",
"experience": "VP of Sales",
"sourceCompany": "stripe"
}In Full mode the same record expands to include work history, education, and skills:
{
"fullName": "Priya Mehta",
"headline": "VP of Sales at Stripe",
"url": "https://www.linkedin.com/in/priyamehta/",
"publicIdentifier": "priyamehta",
"location": "San Francisco, California, United States",
"about": "Scaling revenue operations at Stripe...",
"followersCount": 3420,
"experience": [
{
"position": "VP of Sales",
"companyName": "Stripe",
"startDate": {"year": 2022},
"duration": "4 yrs",
"location": "San Francisco, CA"
},
{
"position": "Director of Sales, APAC",
"companyName": "Twilio",
"startDate": {"year": 2018},
"endDate": {"year": 2022},
"duration": "4 yrs"
}
],
"skills": ["Revenue Operations", "Enterprise Sales", "SaaS"],
"sourceCompany": "stripe"
}fullName and url are the stable CRM keys. headline is the most reliable source for current title — LinkedIn shows it on the company employees page regardless of how the user formatted their experience section. sourceCompany ties the record back to the account that was queried, which is essential for multi-account runs.
Common pitfalls
Three failure modes appear consistently in CRM enrichment pipelines built on LinkedIn data.
Ambiguous company names produce false positives. Querying "Apple" or "Amazon" returns employees from multiple subsidiaries and unrelated companies with similar names. Use LinkedIn company URLs — for example, https://www.linkedin.com/company/apple/ — for any account where precision matters. The actor accepts both formats; prefer URLs for your top-priority accounts.
Stale records from former employees. A small percentage of LinkedIn users do not update their employer when they leave. Treat the scraped data as a starting point, not a definitive source of truth. Cross-check with a profile enrichment step — using the LinkedIn Profile Scraper — before sending outbound sequences to the highest-value contacts.
CRM field mapping mismatches. LinkedIn's headline field is a free-text string set by the user, not a normalized title. Parsing "VP of Sales at Stripe" reliably requires handling variations like "Head of Sales | Stripe" or "VP Sales, Fintech @ Stripe". Apply a normalization step before writing to structured CRM title fields.
The actor handles the anti-bot work and proxy rotation so you pay only for the data, not for browser compute overhead. For the enriched Full-mode output, pair with the LinkedIn Profile Scraper to add skills, certifications, and complete work history to CRM records.
Related use cases
- Scrape LinkedIn company employees for org mapping
- Build account-based marketing from LinkedIn companies
- Enrich your CRM with LinkedIn profile data
- Build a candidate shortlist from LinkedIn profiles
- Track headcount changes at target accounts
- The complete guide to scraping job boards
- All Thirdwatch use-case guides
Frequently asked questions
What CRMs does this pipeline support?
Any CRM with a REST API — HubSpot, Salesforce, Pipedrive, Attio, and Notion databases are the most common targets. The enrichment pattern is always the same: pull employee JSON from Apify, map fields to your CRM Contact schema, POST via the CRM's contact upsert endpoint.
How do I handle duplicate contacts in the CRM?
Use the LinkedIn profile URL as a stable unique key. Before posting each contact, check whether a record with that linkedin_url property already exists; update if found, insert if not. Most CRMs support upsert via a custom unique property on their contacts endpoint.
How often should I re-enrich CRM accounts?
Monthly cadence works for most sales pipelines — LinkedIn headline and location update on average every 3-6 months. For accounts in active deal cycles, re-enrich weekly to catch role changes or departures among key contacts. Use Apify Schedules to automate the cadence without manual intervention.
Does the actor require LinkedIn login or Sales Navigator?
No. The actor works from public LinkedIn data and requires no LinkedIn credentials, cookies, or Sales Navigator seat. The enriched fields reflect publicly visible profile information — name, headline, location, current position, and in Full mode, work history, skills, and education.
What is the difference between Basic and Full mode for CRM enrichment?
Basic mode returns name, headline, profile URL, current position, and source company — sufficient for initial prospecting and CRM contact creation. Full mode adds work history, education, skills, certifications, and languages, which is useful for scoring lead quality or personalising outreach sequences. Start with Basic and escalate to Full only for high-priority accounts.
How do I identify decision-makers in the returned data?
Filter the headline field for seniority keywords such as VP, Director, Head of, Chief, or Manager using a regex applied post-fetch. The jobTitle input parameter narrows results before scraping, which is more efficient than post-filtering when you already know which title to target.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.