Enrich a Lead List with Company Firmographics: A Python Tutorial (2026)
A lead list almost always arrives thin. A form fill gives you an email and a company name. A trade-show scan gives you a badge. A CSV from a partner gives you domains and nothing else. Before your sales or ops team can prioritize any of it, someone has to answer the boring but decisive questions: how big is this company, what industry is it in, where is it, and is it even worth a call?
That is firmographic enrichment, and this is a hands-on tutorial for doing it in Python. We will take a CSV of company leads, call one API per company to pull industry, employee count, size band, founded year, and headquarters, and write an enriched CSV back out. No LinkedIn login, no headless browser, no manual copy-paste from company pages.
But the real subject of this post is the step almost every enrichment tutorial skips: validation. When I ran the code below against eight real companies on July 2, 2026, four enriched cleanly and four did not — one resolved to an entirely different company, one to a look-alike squatter page, and one came back too thin to trust. If you write those four straight into your CRM, you have just poisoned your own data. So we will build the confidence check that catches them.
TL;DR: Loop your lead list, call GET /api/linkedin/company?slug=<slug> with an X-API-Key header, and merge the returned firmographics into each row. Then validate: compare the returned name and website domain against what you expected, tag each row with a confidence tier, and route anything that does not match to review instead of straight into your CRM. Company data is $3.00/1K on Default, down to $0.15/1K at Scale — enriching 10,000 companies is a one-time $30, or $1.50 at Scale. It reads publicly available LinkedIn data only; no login required. Full working code below.
What firmographic enrichment actually is
Firmographics are to companies what demographics are to people: the structured attributes that let you segment, score, and route. For a B2B lead, the ones that matter most are industry, headcount, size band, founded year, and location. Enrichment is the act of starting from a thin identifier — a name, a domain, a LinkedIn slug — and filling those attributes in automatically.
LinkedIn is the natural source because companies maintain their own pages there, so the data is current and self-described. Serpent's company API reads that public page and returns it as a clean JSON object, which means the whole enrichment job becomes an ordinary loop: for each lead, one HTTP request, merge the fields, move on. The interesting engineering is not the request — it is deciding which responses you can trust.
One call per company: the endpoint and fields
There is a single endpoint and one required parameter. Pass either the bare company slug or the full LinkedIn company URL — both resolve to the same page.
GET https://apiserpent.com/api/linkedin/company?slug=<company-slug>
# or, equivalently:
GET https://apiserpent.com/api/linkedin/company?url=https://www.linkedin.com/company/<company-slug>
Header: X-API-Key: YOUR_API_KEY
The response is {"success": true, "data": { ... }}. For enrichment, the fields you care about are these:
| Field | Example | Use in enrichment |
|---|---|---|
name | "Stripe" | Validate the match; display name |
industry | "Software Development" | Segment and route |
employee_count | 15924 | Size scoring (associated members) |
company_size | "5,001-10,000 employees" | SMB / mid-market / enterprise band |
founded_year | 2010 | Company maturity (often null) |
website | "https://stripe.com" | Validate against the lead's domain |
hq | {city, state, country} | Territory routing |
Two of these fields — name and website — are worth a second look, because they are not just data to store. They are the two signals you will use to check that the record you got back is actually the company you asked about.
The naive enrichment loop (works, until it doesn't)
Here is the version most people write first. Read a list of slugs, call the endpoint for each, keep the fields. It is perfectly correct — and it is a trap, for reasons we will see in a moment.
import csv, requests
API_KEY = "YOUR_API_KEY"
BASE = "https://apiserpent.com/api/linkedin/company"
LEADS = ["stripe", "figma", "datadog", "cloudflare", "hashicorp"]
with open("enriched.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["slug", "name", "industry", "employee_count", "company_size"])
for slug in LEADS:
r = requests.get(BASE, headers={"X-API-Key": API_KEY},
params={"slug": slug}, timeout=60)
d = r.json().get("data", {})
w.writerow([slug, d.get("name"), d.get("industry"),
d.get("employee_count"), d.get("company_size")])
For those five slugs, this produces exactly what you want. The problem shows up the moment your list contains a slug that does not map cleanly to the company you meant — and in any real lead list, some of them will not. To see why, let us run a slightly bigger list for real.
A real run: eight leads, four surprises
I took eight well-known companies, used the obvious slug for each, and ran the enrichment. Here is exactly what came back on July 2, 2026, unedited:
| Lead (slug) | Returned name | employee_count | Returned website | Verdict |
|---|---|---|---|---|
| stripe | Stripe | 15,924 | stripe.com | ✅ clean |
| datadog | Datadog | 9,715 | datadoghq.com | ✅ clean |
| cloudflare | Cloudflare | 7,482 | cloudflare.com | ✅ clean |
| hashicorp | HashiCorp | 2,207 | hashicorp.com | ✅ clean |
| figma | Figma | 3,056 | figma.bot | ⚠️ right company, odd domain |
| notion | Notion | 38 | (none) | ⚠️ thin record |
| twilio | TWILIO | (none) | bizzzdev.com | ❌ look-alike page |
| gitlab | Domify | 3 | domify.io | ❌ wrong company entirely |
Method: one live GET /api/linkedin/company?slug=<slug> per lead on 2026-07-02, fields copied verbatim. LinkedIn's public company namespace is a flat list of slugs, and not every obvious guess maps to the company you have in mind.
The four clean rows are the easy 50%. The other four are the reason validation exists:
gitlabreturned "Domify" — a three-person company in Estonia. The slug I guessed simply belongs to a different organization. The naive loop would have written a three-employee Estonian firm into my CRM as "GitLab".twilioreturned "TWILIO" — the name matches, so a name-only check would wave it through. But the website isbizzzdev.comand there is no employee count. This is a look-alike page, not the real company.notioncame back thin — a real-looking name but no website and a garbled industry. Right ballpark, not trustworthy.figmais the honest edge case — it is Figma (3,056 employees, founded 2012), but its website field pointed at a campaign microsite (figma.bot) rather thanfigma.com. A strict domain check will flag it. That is fine: flagged means "look," not "discard."
The takeaway is blunt. You cannot trust a returned record just because you got a 200. The API faithfully returns whatever company owns that slug; deciding whether it is your company is your job.
The step everyone skips: a confidence check
The fix is a small function that scores each record against what you expected for the lead. You already know two things about every lead before you enrich it: roughly what the company is called, and its domain (you almost always have the domain — it is in the email address). So check the response against both.
from urllib.parse import urlparse
def root_domain(url):
if not url:
return ""
host = urlparse(url if "//" in url else "http://" + url).netloc.lower()
return host[4:] if host.startswith("www.") else host
def confidence(name, domain, data):
"""Validate a returned record against what we expected for this lead."""
if not data or not data.get("name"):
return "no_match" # nothing came back
name_ok = name.lower() in data["name"].lower() or data["name"].lower() in name.lower()
got_domain = root_domain(data.get("website"))
domain_ok = bool(got_domain) and (got_domain == domain
or got_domain in domain or domain in got_domain)
rich = bool(data.get("industry")) and data.get("employee_count") is not None
if not name_ok:
return "wrong_entity" # a different company answered to the slug
if domain_ok and rich:
return "high" # name, domain and firmographics all line up
if got_domain and rich:
return "review" # real record, but the domain doesn't match
return "low" # thin record — verify before you use it
Run the eight leads through this and every problem row from the table above sorts itself into the right bucket:
| Tier | Leads | What to do |
|---|---|---|
high | stripe, datadog, cloudflare, hashicorp | Auto-accept — write to CRM |
review | figma | Real company, odd domain — glance and approve |
low | notion, twilio | Thin or suspicious — verify by hand |
wrong_entity | gitlab → "Domify" | Reject — the slug is wrong, re-map it |
No single check is perfect, and it is worth being honest about that. The name test alone let the twilio look-alike through; the domain test alone flagged the genuine Figma. Used together as tiers, they give you the one thing that matters operationally: a clean pile you can automate and a flagged pile a human glances at. That is a far better outcome than a single true/false that is confidently wrong 12% of the time.
The complete enrichment script
Here is the whole thing end to end: it reads a lead list (name, slug, and the domain you already have), enriches each with retry handling, validates every row, writes an enriched CSV with a confidence column, and prints a one-line summary. It uses only the standard library plus requests.
import csv, time, requests
from urllib.parse import urlparse
API_KEY = "YOUR_API_KEY"
BASE = "https://apiserpent.com/api/linkedin/company"
# A lead list from your CRM: company name, LinkedIn slug, and the domain
# you already have (from the work email that came in).
LEADS = [
("Stripe", "stripe", "stripe.com"),
("Figma", "figma", "figma.com"),
("Datadog", "datadog", "datadoghq.com"),
("Cloudflare", "cloudflare", "cloudflare.com"),
("HashiCorp", "hashicorp", "hashicorp.com"),
("GitLab", "gitlab", "gitlab.com"),
("Twilio", "twilio", "twilio.com"),
("Notion", "notion", "notion.so"),
]
FIELDS = ["name", "industry", "employee_count", "company_size",
"founded_year", "website", "hq_city", "hq_country"]
def enrich(slug, retries=3):
"""Fetch a firmographic record for one slug, with retry/backoff."""
for attempt in range(retries):
r = requests.get(BASE, headers={"X-API-Key": API_KEY},
params={"slug": slug}, timeout=60)
if r.status_code == 429: # rate limited
time.sleep(int(r.headers.get("Retry-After", 5))); continue
if r.status_code in (500, 503): # transiently busy
time.sleep(2 ** attempt); continue
if r.status_code == 404: # no such company page
return None
r.raise_for_status()
return r.json().get("data", {})
return None
def root_domain(url):
if not url:
return ""
host = urlparse(url if "//" in url else "http://" + url).netloc.lower()
return host[4:] if host.startswith("www.") else host
def confidence(name, domain, data):
if not data or not data.get("name"):
return "no_match"
name_ok = name.lower() in data["name"].lower() or data["name"].lower() in name.lower()
got_domain = root_domain(data.get("website"))
domain_ok = bool(got_domain) and (got_domain == domain
or got_domain in domain or domain in got_domain)
rich = bool(data.get("industry")) and data.get("employee_count") is not None
if not name_ok:
return "wrong_entity"
if domain_ok and rich:
return "high"
if got_domain and rich:
return "review"
return "low"
def run():
kept = flagged = 0
with open("enriched.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["slug", "confidence"] + FIELDS)
for name, slug, domain in LEADS:
d = enrich(slug) or {}
hq = d.get("hq") or {}
conf = confidence(name, domain, d)
row = [slug, conf, d.get("name"), d.get("industry"),
d.get("employee_count"), d.get("company_size"),
d.get("founded_year"), d.get("website"),
hq.get("city"), hq.get("country")]
w.writerow(["" if v is None else v for v in row])
kept += conf == "high"
flagged += conf != "high"
print(f"{slug:12} {conf:12} -> {d.get('name')}")
time.sleep(0.3) # gentle pacing
print(f"\n{kept} auto-accepted, {flagged} flagged for review")
if __name__ == "__main__":
run()
Point LEADS at your own list, drop in your key, and run it. Rows tagged high are safe to sync automatically; everything else lands in a short review queue you can clear in minutes. Because a missing or wrong company becomes a flagged row rather than a crash or a silent bad write, you can leave the job running over a large list unattended.
Want to run this now? Create a key in under a minute — new accounts include 10 free Google searches to explore the platform. Grab a free key, paste it into the script, and enrich your first list.
Rate limits, retries and scaling up
The script already handles the codes you will actually hit. The endpoint uses neutral, predictable statuses so your loop logic stays simple:
| Status | Meaning | What the script does |
|---|---|---|
200 | Success | Merge fields, validate |
400 | Missing url/slug | Fix the request; don't retry |
404 | No such company page | Return None → flagged row |
429 | Rate limit for your bracket | Honor Retry-After, retry |
503 | Temporarily unavailable | Exponential backoff, retry |
Your rate limit is per-account and scales with your balance bracket, so the way to enrich a big list faster is to keep a working balance rather than to hammer one key with parallel threads. For a few thousand companies, the single-threaded loop above with a small sleep is more than fast enough and stays comfortably inside the limits. If you enrich the same accounts repeatedly, cache by universal_name_id (the stable slug) and only re-fetch on a schedule — headcount and firmographics move slowly, so a weekly refresh is plenty. See the API docs for the full rate-limit semantics.
One more scaling note: enrichment pairs naturally with scoring. Once every row carries industry, employee_count, and company_size, you have exactly the inputs a firmographic lead-scoring model needs — enrich first, then score, then route.
Pricing
Company is the richest LinkedIn dataset and is priced accordingly, but it is pay-as-you-go with no subscription and no per-seat fee — you pay per company enriched.
| Tier | Unlock | Company price | Enrich 10,000 companies |
|---|---|---|---|
| Default | None | $3.00 / 1,000 | $30.00 |
| Growth | One $100 deposit (10× off) | $0.30 / 1,000 | $3.00 |
| Scale | One $500 deposit (20× off) | $0.15 / 1,000 | $1.50 |
Because you only call the API for rows you actually want enriched — and you cache the stable ones — a large one-time backfill plus an ongoing trickle of new leads costs very little. The same deposit unlocks the discount across every endpoint, not just company. See the full pricing page for the other LinkedIn datasets and the SERP APIs.
A note on compliance
Serpent's company endpoint accesses publicly available LinkedIn data only. There is no login, password, cookie, or credential in the flow — you send a public company URL and your Serpent key, and nothing else. That keeps the integration clean, but it does not automatically make every downstream use permitted.
Whether your specific use is allowed depends on LinkedIn's terms, your jurisdiction, and how you store and apply the data. Enriching public company firmographics for B2B routing and scoring is a common, well-trodden use case, but treat compliance as your own decision rather than something this article waves through. The API only ever touches public data and never authenticates as a user; you remain responsible for lawful use.
Enrich your lead list with one API call per company
Industry, employee count, size, founded year and HQ — merged into your CSV or CRM, with a validation step that keeps bad matches out. Company data from $3.00/1K, dropping to $0.15/1K at Scale, pay-as-you-go with no subscription.
Get Your Free API KeyExplore: LinkedIn API · Social Media APIs · Pricing
FAQ
What is firmographic enrichment?
Firmographic enrichment is the process of taking a thin record about a company — often just a name and a domain — and filling in structured attributes like industry, employee count, size band, founded year, and headquarters. It is the company-level equivalent of contact enrichment. With an API you do it programmatically: send an identifier, get back a firmographic object, and write those fields into your CRM or lead list.
What fields can I enrich a company lead with?
Serpent's company endpoint returns name, industry, employee_count (LinkedIn's associated-member figure), company_size band, founded_year, company_type, website, a headquarters object (city, state, country), tagline, description, and logo URL. Some fields are occasionally null — founded_year in particular — so your enrichment code should treat missing fields as normal rather than as errors.
How do I handle wrong or incomplete enrichment results?
Never trust a returned record blindly. A slug can resolve to the wrong company, or to a thin page with a name but no real firmographics. Add a validation step: compare the returned name and website domain against what you expected for the lead, and assign a confidence tier. Auto-accept high-confidence rows, and route anything that does not match — wrong name, mismatched domain, or missing key fields — to manual review instead of writing it straight into your CRM.
How much does firmographic enrichment cost with the API?
Company data is $3.00 per 1,000 requests on Default. A single $100 deposit unlocks Growth at 10× off ($0.30/1,000) and a single $500 deposit unlocks Scale at 20× off ($0.15/1,000). Enriching a 10,000-company list is a one-time $30 on Default or $1.50 at Scale. It is pay-as-you-go with no subscription, and new accounts include free calls to test.
Do I need a LinkedIn login to enrich company data?
No. You send a public company URL or slug and your Serpent API key in an X-API-Key header. The company endpoint accesses publicly available LinkedIn data only — there is no LinkedIn login, cookie, or OAuth flow on your side. You remain responsible for using the enriched data lawfully and within LinkedIn's terms.



