How to Scrape LinkedIn Company Data: Firmographics via API (2026)

By Serpent API Team · · 13 min read

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 exact endpoint, the full response shape field by field, working code in cURL, Python and Node, a resilient loop to enrich a whole CSV of company URLs, the use cases that actually move revenue, honest pricing, and a straight answer on compliance.

TL;DR: Call GET /api/linkedin/company?url=<company url> with an X-API-Key header and you get back a structured firmographic profile — name, industry, company_size, exact employee_count, follower_count, founded_year, website, description, specialities[], company_type, full HQ address, every office location, logo and tagline. It is the richest LinkedIn dataset Serpent exposes. Company data is $3.00/1K on Default, dropping to $0.30/1K (Growth) or $0.15/1K (Scale). No LinkedIn login required; you scrape publicly available LinkedIn data only. 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:

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": { ... }}. Here is a realistic example for a well-known company so you can see the real fields populated. (Values are illustrative — live numbers change.)

An analyst presenting a company-performance dashboard of bar charts and order totals on a screen
{
  "success": true,
  "data": {
    "universal_name_id": "stripe",
    "profile_url": "https://www.linkedin.com/company/stripe",
    "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": 8742,
    "follower_count": 1483920,
    "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": [
      { "city": "South San Francisco", "state": "California", "country": "US", "postal_code": "94080", "line_1": "354 Oyster Point Blvd", "is_hq": true },
      { "city": "Dublin", "state": "Leinster", "country": "IE", "postal_code": "D02 X525", "line_1": "1 Grand Canal Street Lower", "is_hq": false },
      { "city": "Singapore", "state": "", "country": "SG", "postal_code": "048583", "line_1": "80 Robinson Road", "is_hq": false }
    ],
    "logo_url": "https://media.licdn.com/dms/image/v2/C560BAQHHr_Az0u7n0g/company-logo_200_200/0/1656075917000/stripe_logo.png"
  }
}

The field map below is the quick reference you will keep open while you build:

FieldTypeWhat it is
namestringDisplay name of the company.
universal_name_idstringThe URL slug (stable identifier).
industrystringLinkedIn's industry label.
company_sizestringHeadcount band, e.g. "1,001-5,000 employees".
employee_countnumberExact employee count LinkedIn shows.
follower_countnumberPage follower count.
founded_yearnumberYear founded.
websitestringCompany website URL.
descriptionstringThe "About" paragraph.
specialitiesstring[]Self-listed focus areas / tags.
company_typestringe.g. "Public Company", "Privately Held".
hqobjectcity, state, country, postal_code, line_1.
locationsobject[]All offices, each with an is_hq flag.
logo_urlstringCDN URL for the company logo.
taglinestringThe short subline under the name.

A practical note on the two headcount fields: company_size is the band LinkedIn displays (good for bucketing accounts into SMB / mid-market / enterprise), while employee_count is the precise number (good for scoring and for tracking growth over time). Keep both — they answer different questions.

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? Grab a key in under a minute — new accounts include 10 free Google searches to kick the tires across the platform. Create a free key, then point the cURL command above at any 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 bracket.

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; tune to your rate bracket
        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.

A team collaborating around a whiteboard in an office, planning how to use company firmographic data

1. Lead enrichment

A signup form gives you an email and maybe a company name. One call turns that into industry, size band, exact headcount, HQ country and specialities — enough to route the lead, personalize the first touch, and decide whether it deserves a human. Enriching at the point of capture beats buying a static database that goes stale the day you download it.

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

Snapshot employee_count and follower_count for a set of competitors weekly and you have a free headcount-growth and audience-growth tracker. A competitor that quietly grew from 1,200 to 1,900 employees in two quarters is hiring hard — useful context for your own planning, pricing, and positioning. Pair it with the LinkedIn jobs endpoint to see where that growth is going.

Errors, rate limits and retries

Serpent uses neutral, predictable status codes so your batch logic stays simple:

StatusMeaningWhat to do
200Success — {"success": true, "data": {…}}Parse data.
400Missing url/slug parameterFix the request; don't retry.
404Company not foundFlag the row, move on.
429Rate limit for your bracketHonor Retry-After, then retry.
503Temporarily unavailableExponential backoff and retry.

Your rate limit is per-account and scales with your balance bracket, 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.

TierUnlockCompany pricePer call
DefaultNone$3.00 / 1,000$0.0030
GrowthOne $100 deposit (10× off)$0.30 / 1,000$0.0003
ScaleOne $500 deposit (20× off)$0.15 / 1,000$0.00015

So enriching 10,000 companies costs $30 on Default, $3 on Growth, or $1.50 on Scale. New accounts include 10 free Google searches to evaluate the platform end to end; LinkedIn endpoints themselves are paid pay-as-you-go (there is no free LinkedIn call), so the deposit tiers are the way to bring per-call cost down. 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 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 the full firmographic profile — industry, exact headcount, HQ, founded year, specialities and more. No LinkedIn login, no proxies to manage. Company data from $3.00/1K, dropping to $0.15/1K at Scale, pay-as-you-go with no subscription.

Get Your Free API Key

Explore: LinkedIn API · Social Media APIs · Pricing

FAQ

What data does Serpent's LinkedIn company API return?

A single call returns the full firmographic profile of a public LinkedIn company page: name, universal_name_id (the URL slug), industry, company_size band, exact employee_count, follower_count, founded_year, website, description, a specialities array, company_type, headquarters address, an array of all office locations, the logo URL and the tagline. It is the richest LinkedIn dataset Serpent exposes.

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 $3.00 per 1,000 requests on the Default tier. A single $100 deposit unlocks the Growth tier at 10× off ($0.30 per 1,000) and a single $500 deposit unlocks the Scale tier at 20× off ($0.15 per 1,000). New accounts include 10 free Google searches to evaluate the platform; LinkedIn endpoints are pay-as-you-go with no subscription.

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.