Scrape LinkedIn Company Data: Firmographics via API (2026)
If you have ever tried to enrich a sales list, score accounts, or keep a CRM clean, you already know where the pain is: the company data. A name and a website is not enough. You want the firmographics — industry, headcount, headquarters, founding year, the things that decide whether an account is worth a rep's time.
The richest public source of that data is the LinkedIn company page. The problem is that a company page is HTML built for humans, not a feed built for code. This guide shows you how to turn any public LinkedIn company page into clean, structured JSON with a single request using the LinkedIn company data API from Serpent — no login, no cookies, no headless browser to babysit.
We will cover the endpoint, the response shape and field availability, working code in cURL, Python and Node, a loop to enrich a CSV of company URLs, practical use cases, pricing and compliance.
TL;DR: Call GET /api/linkedin/company?url=<company url> with an X-API-Key header for a structured company profile. It includes identity and any public industry, size, website, description, headquarters, founding year, employee count, follower count, logo and tagline the page supplies; optional fields are null when unavailable. Company data is $1.00/1K on Default, dropping to $0.90/1K (Growth) or $0.70/1K (Scale). No LinkedIn login is required. Code in three languages is below.
What a LinkedIn company API actually returns
When people say they want to scrape a LinkedIn company page, they usually mean one of two things: the firmographic snapshot (who this company is) or the activity stream (what they post). For B2B data work, the firmographic snapshot is the one that matters, and it is exactly what Serpent's company endpoint is built around.
Company is the richest of Serpent's LinkedIn datasets. A profile call gives you a person; a jobs call gives you openings; the company call gives you a full organizational fingerprint in one response. Here is the shape of what comes back, before we look at a real example:
- Identity —
name,universal_name_id(the slug in the URL),tagline,descriptionandlogo_url. - Firmographics —
industry,company_size(a band, when shown),employee_countandfollower_count(full numbers only, otherwise null),founded_yearandcompany_type. - Reach & web —
websiteand thespecialitiesarray (the keyword tags a company lists about itself). - Geography — a structured
hqobject with nullable parts. Thelocationsarray is currently empty.
That single object replaces a manual copy-paste, a brittle CSS scraper, or a pricey data broker subscription. And because it is plain JSON, it drops straight into a database column, a CRM field, or a scoring model.
The endpoint: one GET request
There is exactly one endpoint and one required parameter. You can pass either the full company URL or just the slug.
GET https://apiserpent.com/api/linkedin/company?url=<linkedin company url>
# or, equivalently:
GET https://apiserpent.com/api/linkedin/company?slug=<company-slug>
Header: X-API-Key: YOUR_API_KEY
So both of these resolve to the same company:
# By full URL
?url=https://www.linkedin.com/company/stripe
# By slug (the part after /company/)
?slug=stripe
The slug form is convenient when you already have a clean list of identifiers; the URL form is convenient when you are passing through whatever you scraped or exported. Either way, you only ever send a public company URL — there is no LinkedIn session, cookie, or OAuth token involved. Authentication is your Serpent key in the X-API-Key header, and nothing else.
This is part of the broader social media API surface, which sits alongside Serpent's SERP APIs under one key and one billing account.
The JSON response, field by field
Every successful call returns {"success": true, "data": { ... }}. This schema illustration shows the field shape. Values are illustrative; optional fields can be null when the public page does not supply a complete value.
{
"success": true,
"data": {
"linkedin_internal_id": null,
"universal_name_id": "stripe",
"profile_url": "https://www.linkedin.com/company/stripe",
"confidence": "high",
"requested_slug": null,
"name": "Stripe",
"tagline": "Financial infrastructure to grow your revenue",
"description": "Stripe is a financial infrastructure platform for businesses. Millions of companies — from the world's largest enterprises to the most ambitious startups — use Stripe to accept payments, grow their revenue, and accelerate new business opportunities.",
"website": "https://stripe.com",
"industry": "Software Development",
"specialities": [
"online payments",
"developer tools",
"billing and invoicing",
"fraud prevention",
"financial infrastructure",
"global commerce"
],
"company_size": "5,001-10,000 employees",
"employee_count": null,
"follower_count": null,
"founded_year": 2010,
"company_type": "Privately Held",
"hq": {
"city": "South San Francisco",
"state": "California",
"country": "US",
"postal_code": "94080",
"line_1": "354 Oyster Point Blvd"
},
"locations": [],
"logo_url": null,
"background_cover_image_url": null,
"funding_data": []
}
}
The field map below covers the main company fields. The schema illustration above also shows the identity-confidence and empty-array fields:
| Field | Type | What it is |
|---|---|---|
name | string | Display name of the company. |
linkedin_internal_id | string or null | Numeric organization ID when one matching public marker is available; otherwise null. |
universal_name_id | string | The URL slug (stable identifier). |
industry | string or null | Public industry label, when available. |
company_size | string or null | Published headcount band, when available. |
employee_count | number or null | Exact employee count, when available as a full number. |
follower_count | number or null | Exact page follower count, when publicly shown in full. |
founded_year | number or null | Published founding year, when available. |
website | string or null | Public company website URL, when available. |
description | string or null | Public company description, when available. |
specialities | string[] | Self-listed focus areas; an empty list means none were available. |
company_type | string or null | Published type, such as "Public Company" or "Privately Held". |
hq | object | city, state, country, postal_code and line_1; each part may be null. |
locations | object[] | Currently an empty list; office coverage is not advertised. |
logo_url | string or null | Public company logo URL, when available. |
tagline | string or null | Public subline under the name, when available. |
company_size is a published headcount band when available. employee_count is a full number only when the page supplies one; otherwise it is null. Treat missing counts as unknown when scoring or tracking growth.
Working code: cURL, Python, Node
Three languages, same endpoint. Start with cURL to confirm your key works, then move to Python or Node for anything repeatable.
cURL
curl "https://apiserpent.com/api/linkedin/company?url=https://www.linkedin.com/company/stripe" \
-H "X-API-Key: YOUR_API_KEY"
Python (requests)
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://apiserpent.com/api/linkedin/company"
def get_company(identifier, timeout=60):
"""identifier can be a full LinkedIn company URL or a bare slug."""
param = "url" if identifier.startswith("http") else "slug"
r = requests.get(
BASE,
headers={"X-API-Key": API_KEY},
params={param: identifier},
timeout=timeout,
)
r.raise_for_status()
payload = r.json()
if not payload.get("success"):
raise RuntimeError(payload.get("message", "request failed"))
return payload["data"]
company = get_company("https://www.linkedin.com/company/stripe")
print(company["name"], "—", company["employee_count"], "employees")
print("HQ:", company["hq"]["city"], company["hq"]["country"])
print("Specialities:", ", ".join(company["specialities"][:5]))
Node (fetch)
const API_KEY = "YOUR_API_KEY";
const BASE = "https://apiserpent.com/api/linkedin/company";
async function getCompany(identifier) {
const param = identifier.startsWith("http") ? "url" : "slug";
const u = new URL(BASE);
u.searchParams.set(param, identifier);
const res = await fetch(u, { headers: { "X-API-Key": API_KEY } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const payload = await res.json();
if (!payload.success) throw new Error(payload.message || "request failed");
return payload.data;
}
const company = await getCompany("https://www.linkedin.com/company/stripe");
console.log(`${company.name} — ${company.employee_count} employees`);
console.log(`Founded ${company.founded_year}, type: ${company.company_type}`);
That is the whole integration. No browser automation, no proxy rotation, no parsing of constantly-changing HTML on your side — the API returns the same stable schema for every company.
Want to try it before you write a loop? New accounts have 10 shared eligible free calls, including LinkedIn Company. Create a free key, then point the cURL command above at a public company page.
Enrich a CSV of companies at scale
The real value shows up when you run this over a list. Say you exported 5,000 company URLs from your CRM and want a firmographic column for each. Because the endpoint is one GET per company, enrichment is just a loop with sane error handling and a small delay to stay friendly with your rate limit.
Python: read a CSV in, write an enriched CSV out
import csv, time, requests
API_KEY = "YOUR_API_KEY"
BASE = "https://apiserpent.com/api/linkedin/company"
FIELDS = [
"input", "name", "industry", "company_size", "employee_count",
"follower_count", "founded_year", "website", "company_type",
"hq_city", "hq_country", "specialities", "error",
]
def fetch(identifier, retries=3):
param = "url" if identifier.startswith("http") else "slug"
for attempt in range(retries):
r = requests.get(BASE, headers={"X-API-Key": API_KEY},
params={param: identifier}, timeout=60)
if r.status_code == 429: # rate limited
wait = int(r.headers.get("Retry-After", 5))
time.sleep(wait)
continue
if r.status_code == 404:
return {"error": "not found"}
if r.status_code == 503: # temporarily busy
time.sleep(2 ** attempt)
continue
r.raise_for_status()
return r.json().get("data", {})
return {"error": "exhausted retries"}
def flatten(identifier, d):
if "error" in d:
return {"input": identifier, "error": d["error"]}
hq = d.get("hq") or {}
return {
"input": identifier,
"name": d.get("name"),
"industry": d.get("industry"),
"company_size": d.get("company_size"),
"employee_count": d.get("employee_count"),
"follower_count": d.get("follower_count"),
"founded_year": d.get("founded_year"),
"website": d.get("website"),
"company_type": d.get("company_type"),
"hq_city": hq.get("city"),
"hq_country": hq.get("country"),
"specialities": "; ".join(d.get("specialities") or []),
"error": "",
}
with open("companies.csv") as fin, open("enriched.csv", "w", newline="") as fout:
writer = csv.DictWriter(fout, fieldnames=FIELDS)
writer.writeheader()
for row in csv.reader(fin):
identifier = row[0].strip()
if not identifier:
continue
try:
data = fetch(identifier)
except Exception as e:
data = {"error": str(e)}
writer.writerow(flatten(identifier, data))
time.sleep(0.3) # gentle pacing between calls
print("done:", identifier)
Node: the same loop, promise-based
import fs from "node:fs";
const API_KEY = "YOUR_API_KEY";
const BASE = "https://apiserpent.com/api/linkedin/company";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function fetchCompany(identifier, retries = 3) {
const param = identifier.startsWith("http") ? "url" : "slug";
for (let i = 0; i < retries; i++) {
const u = new URL(BASE);
u.searchParams.set(param, identifier);
const res = await fetch(u, { headers: { "X-API-Key": API_KEY } });
if (res.status === 429) { await sleep((+res.headers.get("Retry-After") || 5) * 1000); continue; }
if (res.status === 404) return { error: "not found" };
if (res.status === 503) { await sleep(2 ** i * 1000); continue; }
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()).data;
}
return { error: "exhausted retries" };
}
const inputs = fs.readFileSync("companies.csv", "utf8").split("\n").map((s) => s.trim()).filter(Boolean);
const rows = [];
for (const id of inputs) {
try {
const d = await fetchCompany(id);
rows.push([id, d.name ?? "", d.industry ?? "", d.employee_count ?? "", (d.hq?.country) ?? "", d.error ?? ""].join(","));
} catch (e) {
rows.push([id, "", "", "", "", e.message].join(","));
}
await sleep(300);
console.log("done:", id);
}
fs.writeFileSync("enriched.csv", "input,name,industry,employee_count,hq_country,error\n" + rows.join("\n"));
Both versions degrade gracefully: a not-found company becomes a flagged row instead of crashing the batch, a 429 backs off and retries, and a transient 503 uses exponential backoff. That is the difference between a script you babysit and one you can leave running.
What to build with company firmographics
Structured company data is a means, not an end. Here are the four jobs it does best.
1. Lead enrichment
A signup form gives you an email and maybe a company name. A company call can add public industry, size band, HQ country and specialities when available; exact headcount may be null. Use the fields present to route a lead and flag unknowns for follow-up.
2. ABM and account scoring
Account-based marketing lives or dies on its target list. Pull employee_count, industry, company_type and founded_year for every account in your TAM, then score: a privately held software company of 200–1,000 employees founded in the last decade is a very different bet than a 50,000-person public incumbent. The specialities array is an underused signal here — it tells you what a company says it cares about, in its own words.
3. CRM hygiene
CRMs rot. Headcounts change, companies relocate, names get rebranded. Re-running the company endpoint on a schedule lets you refresh the firmographic fields on existing records, flag accounts that crossed a size threshold, and catch HQ moves. Because universal_name_id is a stable key, you can de-duplicate records that point at the same company under different names.
4. Competitor and market tracking
Where companies publish full employee_count and follower_count values, weekly snapshots can show headcount and audience changes. A null value means the public page did not provide a complete number, so leave that interval out of a growth calculation. Pair available counts with the LinkedIn jobs endpoint for more context.
Errors, rate limits and retries
Serpent uses neutral, predictable status codes so your batch logic stays simple:
| Status | Meaning | What to do |
|---|---|---|
200 | Success — {"success": true, "data": {…}} | Parse data. |
400 | Missing url/slug parameter | Fix the request; don't retry. |
404 | Company not found | Flag the row, move on. |
429 | Automated overuse or temporary server load | Honor Retry-After, then retry. |
503 | Temporarily unavailable | Exponential backoff and retry. |
Rate limits are per-account and scale with your balance, so the simplest way to run larger batches faster is to keep a working balance rather than to hammer a single key. The loops above already implement the right behavior for each code; the only knob you usually need to tune is the inter-request sleep. Full status semantics live in the API docs.
Pricing
Company is the richest LinkedIn dataset, and it is priced accordingly — but it is still pay-as-you-go with no subscription and no per-seat fees.
| Tier | Unlock | Company price | Per call |
|---|---|---|---|
| Default | None | $1.00 / 1,000 | $0.0010 |
| Growth | Single $100 deposit (10% off) | $0.90 / 1,000 | $0.0009 |
| Scale | Single $500 deposit (30% off) | $0.70 / 1,000 | $0.0007 |
After free calls, 10,000 delivered Company calls cost $10 on Default, $9 on Growth, or $7 on Scale. New accounts have 10 shared eligible free calls, including LinkedIn Company. A qualifying single deposit locks a discount for later paid calls; see the full pricing page for the other LinkedIn datasets and SERP APIs.
A note on compliance
Serpent's LinkedIn company API accesses publicly available LinkedIn data only. There is no login, no password, no cookie, and no credential of any kind 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 make every use automatically permitted.
Whether your specific use is allowed depends on LinkedIn's terms, your jurisdiction, and how you store and apply the data. Public B2B firmographics for enrichment and account research is a common and well-trodden use case, but treat compliance as your own decision rather than something this article waves through. In short: the API only ever touches public data and never authenticates as a user, and you remain responsible for lawful use.
If you are weighing this against a dedicated profile-data vendor, our Proxycurl alternative guide walks through the trade-offs of moving LinkedIn enrichment onto a general-purpose API.
Turn any LinkedIn company page into clean JSON
One GET request returns company identity and available public firmographics such as industry, HQ, founding year and specialities. Missing optional values remain null. No LinkedIn login is needed. Company data starts at $1.00/1K, dropping to $0.70/1K at Scale.
Get Your Free API KeyExplore: LinkedIn API · Social Media APIs · Pricing
FAQ
What data does Serpent's LinkedIn company API return?
A successful call returns the company name and URL identity plus available public firmographics such as industry, company size, website, description and headquarters. Employee and follower counts, founding year, logo and other optional fields are null when a full value is unavailable. The locations list is currently empty.
Do I need a LinkedIn login, cookies, or OAuth to use it?
No. You authenticate to Serpent with a single X-API-Key header and pass a public company URL or slug. The LinkedIn company API accesses publicly available LinkedIn data only — there is no LinkedIn login, password, cookie, or OAuth flow to manage on your side. You are responsible for using the data lawfully and within LinkedIn's terms.
How much does the LinkedIn company data API cost?
Company data is $1.00 per 1,000 requests on the Default tier. A qualifying single $100 deposit locks Growth pricing ($0.90 per 1,000); a single $500 deposit locks Scale pricing ($0.70 per 1,000). New accounts have 10 shared eligible free calls, including LinkedIn Company, before pay-as-you-go credits apply.
Can I enrich a whole CSV of companies at once?
Yes. The endpoint is one GET request per company, so you loop over a list of company URLs or slugs and write each JSON response to a row. The Python and Node examples in this guide show a resilient loop with retries, rate-limit handling and CSV output that you can point at thousands of companies.
Is scraping LinkedIn company data allowed?
Serpent's API accesses publicly available LinkedIn company pages only and never uses any login or credentials. Whether your specific use is permitted depends on LinkedIn's terms, your jurisdiction and how you store and use the data, so you remain responsible for lawful use. Public company firmographics for B2B enrichment is a common use case, but treat compliance as your own decision rather than a blanket rule.



