# LinkedIn Company Jobs Scraper — Jobs, Salary & Company Data (`foxlabs/linkedin-company-jobs-scraper`) Actor

All open jobs of the companies you list, from LinkedIn's public job pages, no login. Title, location, posted date, seniority, employment type, function, salary, applicants and full description, plus company firmographics. Filter by keyword, location, date, seniority; monitor new jobs.

- **URL**: https://apify.com/foxlabs/linkedin-company-jobs-scraper.md
- **Developed by:** [Berkan Kaplan](https://apify.com/foxlabs) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## LinkedIn Company Jobs Scraper — Jobs, Salary & Company Data

Give it a list of companies; get their open LinkedIn jobs as clean rows — title, location, posted date, seniority, employment type, job function, applicant count, the full description, **salary** and the **company's data on every row**. No LinkedIn account, no cookies: only LinkedIn's public job pages.

What makes it different, measured on the platform:

- **Salary where employers actually state it.** LinkedIn's own pay box was filled for only 53 of 897 jobs in our test runs; most employers write the range into the description instead ("The annual US base salary range for this role is $126,600 – $190,000"). The Actor reads both, under strict rules, and tells you where each figure came from (`salarySource`, `salaryRaw`). Salary was found for **99 % of US-located jobs** (493 of 498; mostly California, New York and Washington, which require pay ranges), 30 % of jobs elsewhere, 68 % overall.
- **Worldwide by default.** Without a location, LinkedIn quietly searches the United States only (Stripe: 625 US jobs vs 942 worldwide). This Actor always asks for worldwide unless you name a place.
- **LinkedIn's own job count on every row** (`linkedinJobCount`) — a hiring-volume signal, and a check that you got everything.
- **Seniority and job-type filters that actually filter.** LinkedIn ignores these filters on its public pages (we checked: the results do not change). This Actor applies them to what each job page states. About half of the employers we tested state no seniority at all; the run tells you when that is the case.

### Quick start (API)

```bash
curl -X POST "https://api.apify.com/v2/acts/foxlabs~linkedin-company-jobs-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies":["https://www.linkedin.com/company/vercel/","Stripe"],"keywords":"engineer","datePosted":"past-week","maxJobsPerCompany":50}'
```

### What you get

| Field | What it is |
|---|---|
| `title`, `location`, `postedDate`, `postedTimeAgo`, `jobUrl`, `jobId` | From the job listing |
| `seniorityLevel`, `employmentType`, `jobFunction`, `industries` | As LinkedIn states them on the job page |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | Parsed pay (yearly / monthly / hourly; cents kept) |
| `salarySource`, `salaryRaw` | `linkedin` (LinkedIn's pay box) or `description` (the employer's own sentence, quoted in `salaryRaw`) |
| `applicantCount`, `applicantCountText`, `applicantCountIsLowerBound`, `applicantCountIsUpperBound` | "Over 200 applicants" is a floor, "Be among the first 25" a ceiling |
| `descriptionText`, `descriptionHtml` | Full description as readable text (line breaks and bullets kept) and as HTML |
| `listingBadge`, `benefits` | "Actively Hiring" / "Be an early applicant"; a benefits line when the listing has one (rare) |
| `companyName`, `companyId`, `companyLinkedInUrl`, `companyWebsite`, `companyDomain` | Company identity. `companyWebsite` is as LinkedIn lists it; `companyDomain` is the company's registrable domain (news.microsoft.com → microsoft.com; a campaign link such as figma.bot/… is followed to figma.com) |
| `companyIndustry`, `companyEmployeeCount`, `companyFollowers`, `companyHQ`, `companyFounded` | Company firmographics from its LinkedIn page, on every row |
| `linkedinJobCount`, `linkedinJobCountIsLowerBound` | LinkedIn's own count of open jobs for the company and your search ("2,000+" is a floor) |
| `detailStatus`, `companyResolvedBy`, `query`, `isNew`, `scrapedAt` | Provenance: whether the job page was read, how the company was identified, which input it came from, whether a monitor saw it before |

#### Sample output

A real row from a platform run (description shortened):

```json
{
  "companyName": "Vercel",
  "companyId": "16181286",
  "companyDomain": "vercel.com",
  "companyIndustry": "Software Development",
  "companyEmployeeCount": 1027,
  "companyHQ": "San Francisco, California, US",
  "companyFounded": 2015,
  "linkedinJobCount": 32,
  "jobId": "4460361914",
  "title": "Software Engineer, Financial Data Platform",
  "location": "New York, United States",
  "postedDate": "2026-09-19",
  "seniorityLevel": "Not Applicable",
  "employmentType": "Full-time",
  "jobFunction": "Other",
  "industries": "Software Development",
  "salaryMin": 190000,
  "salaryMax": 258000,
  "salaryCurrency": "USD",
  "salaryPeriod": "yearly",
  "salaryRaw": "The San Francisco, CA base pay range for this role is $190,000 - $258,000.",
  "salarySource": "description",
  "applicantCount": 25,
  "applicantCountText": "Be among the first 25 applicants",
  "applicantCountIsUpperBound": true,
  "listingBadge": "Be an early applicant",
  "descriptionText": "About Vercel:\n\nVercel is the agentic infrastructure company, …",
  "jobUrl": "https://www.linkedin.com/jobs/view/4460361914",
  "detailStatus": "ok",
  "companyResolvedBy": "slug"
}
```

This row also shows a limit worth knowing: the New York job quotes only the San Francisco range. `salaryRaw` always shows the sentence, so you can see which location a range belongs to.

### Input & filters

| Input | Notes |
|---|---|
| `companies` | LinkedIn company URL (exact), slug (`stripe`), numeric company ID, or name. A name becomes LinkedIn's usual slug and must match the company name on that page — otherwise the run says so instead of returning a namesake's jobs. |
| `keywords` | Applied by LinkedIn |
| `location` | Applied by LinkedIn. Empty = worldwide |
| `datePosted` | Past 24 hours / week / month — applied by LinkedIn |
| `experienceLevel`, `jobType` | Seniority and employment type, several allowed. Applied by the Actor to each job page (LinkedIn ignores these filters on public pages), so full details are switched on and every page read is charged as a job detail, matched or not |
| `maxJobsPerCompany` | Up to 1,000 (LinkedIn's public search stops at about 1,000 per company and search) |
| `scrapeDetails` | Read each job page (description, criteria, salary, applicants). On by default |
| `onlyNewJobs`, `monitorName` | Monitoring: return only jobs this monitor has not returned before |

Slugs are the address of the company page, and single words are read as slugs. They can belong to a namesake: `notion` is a 39-person company called "Notion"; Notion Labs is `notionhq`. When a slug lands on a page with no jobs, the run names that page and its size. When in doubt, paste the URL.

### Example inputs (copy & paste)

Engineering jobs posted this week at three companies:

```json
{ "companies": ["stripe", "vercel", "https://www.linkedin.com/company/notionhq/"], "keywords": "engineer", "datePosted": "past-week", "maxJobsPerCompany": 100 }
```

All of Microsoft's open jobs in Germany, with details:

```json
{ "companies": ["https://www.linkedin.com/company/microsoft/"], "location": "Germany", "maxJobsPerCompany": 1000 }
```

Weekly monitor of ten target accounts (schedule it; each run returns up to 50 jobs per company that it has not returned before — raise the number for busy employers):

```json
{ "companies": ["stripe", "vercel", "https://www.linkedin.com/company/notionhq/", "figma", "datadog", "airbnb", "shopify", "hubspot", "snowflake-computing", "openai"], "datePosted": "past-week", "maxJobsPerCompany": 50, "onlyNewJobs": true, "monitorName": "target-accounts" }
```

Director and executive full-time roles only (Stripe states seniority on every job):

```json
{ "companies": ["https://www.linkedin.com/company/stripe/"], "experienceLevel": ["director", "executive"], "jobType": ["full-time"], "maxJobsPerCompany": 20 }
```

### Use cases

- **Sales and agencies:** open roles are buying signals — a company hiring five data engineers is building a data team. Salary and seniority say how big the budget is.
- **Recruiters:** every open role at your target clients, with pay ranges and applicant counts to prioritise.
- **Investors and analysts:** hiring volume (`linkedinJobCount`) and mix by function, seniority and country, company by company, week by week.
- **Job boards and salary research:** structured jobs with parsed, sourced pay.

### Performance & throughput

Measured on the Apify platform (512 MB memory, residential proxy, 2026-09-24):

| Run | Jobs | Time | Rate |
|---|---|---|---|
| Stripe + Vercel + Microsoft, 200 each, full details | 512 | 342 s | ≈ 90 jobs / min |
| Same, without details (earlier build, one company at a time) | 512 | 214 s | ≈ 144 jobs / min |
| Microsoft in Germany, all jobs (LinkedIn count 12), full details | 12 | 37 s | 12 / 12 delivered |

- Three companies are processed at a time. Each company's job pages are read ten at a time while the next result page loads.
- 512 MB is enough: the Actor only makes HTTP requests and parses HTML, with no browser.
- LinkedIn sometimes refuses a request (HTTP 999 / 429). The Actor retries it on a new residential IP, up to four attempts. In the full-detail run above, 512 of 512 job pages were read. A job whose page could not be read is still delivered, with `detailStatus` `unavailable` or `job-closed`, and is not charged as a detail.

### Data quality

Measured on platform runs (builds 0.1.1–0.1.4, 2026-09-24):

| | Share of jobs |
|---|---|
| Title, location, posted date, job URL | 100 % |
| Company name, ID, domain, industry, employees, HQ | 100 % |
| Seniority, employment type, job function, industries, applicant count, description (details on) | 100 % of jobs whose page was read; pages read 512 / 512 |
| Salary (details on) | 68 % of 897 unique jobs: 99 % of US-located jobs, 30 % elsewhere. By company from 0 % (Shopify, Goldman Sachs samples) to 94 % (Figma); Microsoft Germany 12 of 12 |
| Founding year | 61–100 % — some company pages state none (Microsoft), and none is invented |
| Unique jobs per input | 100 % |

How salary is read:

- LinkedIn's pay box first (`salarySource: "linkedin"`). Otherwise the description (`"description"`), but only in a sentence that names pay ("salary", "pay range", "hourly rate", …) or states a period ("/hr", "per year"), and only with a currency. A lead-in such as "The reasonably estimated yearly salary for this role is:" counts for the line under it.
- Bonus, stipend, funding and revenue amounts are not pay. Implausible values are rejected, and so are ranges wider than 8×.
- A "Pay Range" heading counts for the lines under it, and "Minimum hourly: $35.50" plus "Maximum hourly: $55.60" become one range.
- One stated amount is the exact pay (min = max); "up to $X" fills only the maximum, "starting at $X" only the minimum.
- A range in another country's currency than the job's (a London job quoting its Amsterdam range in euros) is left out.

### Pricing

Pay per event:

- `job` — each job delivered.
- `job-detail` — each job page read: every delivered job with details on, plus, when you use the seniority / job-type filter, the pages of jobs the filter left out.

Companies that are not found or have no jobs cost nothing and leave an explanatory row. The current prices are on the Pricing tab.

### FAQ

**Why is there no remote / hybrid filter?** LinkedIn's public pages ignore the workplace filter (results do not change) and do not state the workplace type reliably, so the Actor offers neither the filter nor a column rather than guess. Use `keywords` (for example "remote") if the word matters to you.

**Why did I get fewer jobs than `linkedinJobCount`?** LinkedIn's public search stops at about 1,000 per company and search. Split big employers by `location`, `keywords` or `datePosted`. `linkedinJobCountIsLowerBound` marks counts LinkedIn shows as "2,000+".

**How do I catch every new job with a monitor?** Each monitoring run returns at most "Max jobs per company" jobs the monitor has not seen, and remembers them. Set it above the company's volume for your search (`linkedinJobCount` on the rows shows it), and use "Posted: past week" for a weekly schedule. Measured: a second run over ten companies returned 157 jobs, none of them returned before.

**Do I need a LinkedIn account or cookies?** No. Only public job pages are read.

**What does a seniority filter cost?** LinkedIn cannot filter its public pages, so the Actor reads the page of every listed job to test it. Each page read is a job detail, matched or not. To keep a rare filter from running up a bill, it reads at most 20 listed jobs per requested job (at least 50) and says so when it stops.

**Why does the seniority filter find nothing at some companies?** Many employers state no seniority on LinkedIn. In our sample of twelve tech companies, six (Vercel, Microsoft, HubSpot, Snowflake, OpenAI, Notion) had "Not Applicable" on every job, while Stripe, Figma, Shopify, Airbnb and Goldman Sachs state it. When the first 20 job pages of a company all say "Not Applicable", the Actor stops that company after those 20 pages and tells you why. Run it without the seniority filter, or add "Not Applicable".

### Troubleshooting

- **"No LinkedIn company page at /company/…/ (made from the name …)"** — the name does not match LinkedIn's slug. Paste the company URL.
- **"… resolved to …"** — the slug belongs to a different company than the name you gave. Paste the company URL.
- **Empty result with a note about the page's size** — the slug is a namesake (see `notion` vs `notionhq` above).
- Every company's outcome is in the run's `SOURCE_REPORT` record: status, LinkedIn's count, jobs delivered, jobs left out by filters, request counters.

### Notes, limits & legal

- Reads public LinkedIn job pages only; no login, no private data. Not affiliated with LinkedIn Corporation.
- No person-level data: LinkedIn shows the hiring team only to signed-in members, and this Actor does not sign in. Job descriptions can still name people; if you store them, handle them lawfully (for example under GDPR).
- LinkedIn changes its pages from time to time; parsers are covered by regression tests on real pages.

### Support

Open an issue on the Actor's Issues tab. Include the run ID; the `SOURCE_REPORT` record usually shows the cause.

### Changelog

See [CHANGELOG.md](./CHANGELOG.md). 0.1 (2026-09-24): first version.

# Changelog

This Actor's version history is a separate document: https://apify.com/foxlabs/linkedin-company-jobs-scraper/changelog.md

# Actor input Schema

## `companies` (type: `array`):

One per line: a LinkedIn company URL (https://www.linkedin.com/company/stripe/), its slug (stripe), its numeric company ID, or the company name. URLs are exact; a name is turned into LinkedIn's usual slug and checked against the page's company name.

## `keywords` (type: `string`):

Only jobs matching these words, e.g. "engineer" or "account executive". Leave empty for all jobs.

## `location` (type: `string`):

City, region or country, e.g. "Germany" or "New York". Leave empty for all locations worldwide (LinkedIn itself defaults to the United States; this Actor asks for Worldwide).

## `datePosted` (type: `string`):

Only jobs posted within this window (applied by LinkedIn).

## `experienceLevel` (type: `array`):

Keep only jobs whose page states one of these seniority levels. LinkedIn's own seniority filter has no effect on its public pages, so the Actor reads each job page (full details are switched on) and every page read is charged as a job detail, whether or not the job matches. Leave empty for all.

## `jobType` (type: `array`):

Keep only jobs whose page states one of these employment types. Like seniority, this is applied by the Actor on each job page, and every page read is charged as a job detail. Leave empty for all.

## `maxJobsPerCompany` (type: `integer`):

LinkedIn's public job search stops at about 1,000 results per company and search; for bigger employers, split by location, keywords or date. Each row carries LinkedIn's own job count so you can see the gap.

## `scrapeDetails` (type: `boolean`):

Open each job for its description, seniority, employment type, job function, industries, applicant count and salary (one extra request per job).

## `onlyNewJobs` (type: `boolean`):

Monitoring mode for scheduled runs: returns only jobs this monitor has not returned before. The first run returns up to "Max jobs per company" and remembers them.

## `monitorName` (type: `string`):

Separate memories for separate monitors (for example one per client). Stored in your own key-value store.

## `proxyConfiguration` (type: `object`):

LinkedIn limits repeated requests from one IP; Apify residential proxy (the default) keeps runs reliable.

## Actor input object example

```json
{
  "companies": [
    "https://www.linkedin.com/company/vercel/",
    "Stripe"
  ],
  "datePosted": "any",
  "maxJobsPerCompany": 100,
  "scrapeDetails": true,
  "onlyNewJobs": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `report` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "https://www.linkedin.com/company/vercel/",
        "Stripe"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("foxlabs/linkedin-company-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "companies": [
        "https://www.linkedin.com/company/vercel/",
        "Stripe",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("foxlabs/linkedin-company-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "https://www.linkedin.com/company/vercel/",
    "Stripe"
  ]
}' |
apify call foxlabs/linkedin-company-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,foxlabs/linkedin-company-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RXb7BIvdSCYhfLXeu/builds/UJUakhCg9C2cl7Asp/openapi.json
