Enrich a Sales CRM with Public LinkedIn Employee Data
Pull public employee profiles for named accounts, qualify them, and upsert reviewed contacts into HubSpot or another CRM.

A CRM enrichment job has two separate parts: find plausible contacts, then decide which records are good enough to write into the CRM. Combining those steps without review is how databases fill up with unrelated people and stale assumptions.
The LinkedIn Company Employees Scraper handles public profile discovery. It returns JSON; it does not silently write into HubSpot or Salesforce.
Run the ready-made Task: Enrich a target-account shortlist.
Pull employee records for named accounts
import os
import requests
actor = "thirdwatch~linkedin-company-employees-scraper"
token = os.environ["APIFY_TOKEN"]
response = requests.post(
f"https://api.apify.com/v2/acts/{actor}/run-sync-get-dataset-items",
params={"token": token},
json={
"queries": [
"https://www.linkedin.com/company/stripe/",
"https://www.linkedin.com/company/datadog/",
],
"mode": "basic",
"jobTitle": "Sales",
"maxResults": 100,
},
timeout=1800,
)
response.raise_for_status()
people = response.json()jobTitle is a search refinement. It is not an exact filter, so inspect currentTitle and headline before writing records.
Choose the fields your CRM actually needs
For a Basic row, the useful mappings are straightforward:
| Actor field | Typical CRM field |
|---|---|
firstName |
First name |
lastName |
Last name |
currentTitle |
Job title |
currentCompany |
Company, when profile-backed; Basic rows can be blank |
url |
LinkedIn URL and deduplication key |
sourceCompany |
Target account that produced the record |
headline |
Research context |
scrapedAt |
Last observed time |
Do not create fake email addresses or locations when those fields are missing.
Filter before upserting
import re
TARGET_ROLES = re.compile(
r"\b(vp|vice president|head of|director|chief|revops|sales operations)\b",
re.I,
)
qualified = []
for person in people:
title = person.get("currentTitle") or person.get("headline") or ""
if TARGET_ROLES.search(title):
qualified.append(person)Your rules should reflect the product and account, not a universal list of "senior" titles.
Upsert by LinkedIn URL
The example below uses HubSpot's CRM API. Create a custom unique text property named linkedin_profile_url before running it. Confirm the property name and API version against your CRM account.
import os
import requests
hubspot_token = os.environ["HUBSPOT_TOKEN"]
base = "https://api.hubapi.com/crm/v3/objects/contacts"
headers = {
"Authorization": f"Bearer {hubspot_token}",
"Content-Type": "application/json",
}
def upsert(person):
linkedin_url = person["url"]
search = requests.post(
f"{base}/search",
headers=headers,
json={
"filterGroups": [{
"filters": [{
"propertyName": "linkedin_profile_url",
"operator": "EQ",
"value": linkedin_url,
}]
}],
"limit": 1,
},
timeout=30,
)
search.raise_for_status()
matches = search.json().get("results", [])
properties = {
"firstname": person.get("firstName", ""),
"lastname": person.get("lastName", ""),
"jobtitle": person.get("currentTitle", ""),
"company": person.get("currentCompany", ""),
"linkedin_profile_url": linkedin_url,
}
if matches:
result = requests.patch(
f"{base}/{matches[0]['id']}",
headers=headers,
json={"properties": properties},
timeout=30,
)
else:
result = requests.post(
base,
headers=headers,
json={"properties": properties},
timeout=30,
)
result.raise_for_status()
return result.json()
saved = [upsert(person) for person in qualified]For large datasets, use your CRM's batch endpoint and rate-limit handling rather than one request per row.
Use Full mode selectively
Full mode attempts public-profile enrichment for work history, education, skills, certifications, languages, location, summary, and public follower count.
Keep sourceCompany as discovery provenance. Do not substitute it for a blank currentCompany: Basic public search can contain false positives, while currentCompany is reserved for employer data LinkedIn actually exposes.
That data is useful when it changes lead qualification. It is wasteful when a Basic headline already tells you the person is irrelevant.
{
"queries": ["https://www.linkedin.com/company/anthropic/"],
"mode": "full",
"jobTitle": "Product Manager",
"maxResults": 20
}Check resultMode. A restricted profile that cannot be enriched is returned and charged as Basic, not Full.
Add email in a separate step
The Actor does not pull private email addresses from LinkedIn. After a person passes role and account review, use the LinkedIn Email Finder or another lawful provider.
Keep the provenance fields. Your CRM should record that a guessed or verified work email came from the enrichment step, not from LinkedIn.
Maintain the CRM with confirmed additions
For a recurring workflow, use monitorMode: "new-employees" with a stable monitorStoreName. The baseline returns all discovered profiles. Later scheduled runs return only unseen profiles confirmed in two consecutive complete snapshots.
When a monitor uses Full mode, the Actor discovers the roster cheaply first and enriches only baseline or confirmed-new rows that it will actually return. Unchanged profiles do not consume residential enrichment on every schedule.
That lets the integration upsert a small change set. It does not identify departures. Public search absence is not enough evidence to delete or close a CRM contact.
Pricing
Basic profiles cost $0.003 to $0.0015 each, Full profiles cost $0.008 to $0.005, and a completed company search costs $0.020 to $0.015 depending on tier.
On FREE, two completed company searches returning 100 Basic profiles each cost about $0.64. An unchanged monitor has company-search charges but no profile charges.
Production checklist
- Review the first company sample for false positives.
- Deduplicate by LinkedIn URL.
- Keep source and observation-time fields.
- Never overwrite a verified CRM value with a blank public field.
- Treat title and company values as observations that may need verification.
- Batch CRM writes and handle rate limits.
- Keep a lawful basis for personal-data processing and honor opt-outs.
Frequently asked questions
Does the Actor write directly to my CRM?
No. It returns JSON rows through an Apify dataset. Send reviewed rows to your CRM with its API, an Apify integration, Make, Zapier, n8n, or your own code.
How should I deduplicate contacts?
Use the canonical LinkedIn profile URL or publicIdentifier. Names are not unique and change more often.
Does the dataset include email addresses?
No. Anonymous public LinkedIn profiles do not expose private emails. Run separate lawful email enrichment only after qualifying a shortlist.
Which mode should I use?
Use Basic for broad discovery and CRM contact creation. Use Full for a smaller shortlist when public work history, education, skills, or location changes lead qualification.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.