# LinkedIn Company Jobs Scraper (No Cookies) (`northbell/linkedin-company-jobs-scraper`) Actor

Give it a company, get every role that company has open right now: title, location, the exact posting date and a link. Company URL, slug or numeric ID. No login, no cookies.

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

## Pricing

Pay per event

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## LinkedIn Company Jobs Scraper

**Give it a company. Get every role that company has open right now.**

Most LinkedIn job scrapers take a *search query*. This one takes a **company**. Paste
`linkedin.com/company/stripe`, get all of Stripe's open roles.

No login. No cookies. No account. Public pages only.

***

### What you get

One row per open role:

| Field | Meaning |
|---|---|
| `title`, `location` | The role and where it is |
| `postedOn` | The real posting date, not "3 days ago" |
| `activelyHiring` | Whether LinkedIn flags the company as actively hiring |
| `jobUrl` | Direct link |

And one summary row per company: `openJobs`, `byLocation`, the resolved `companyId`, and
`collectionComplete` (see below).

***

### How complete is the list?

LinkedIn's public job listing does not paginate the way you would expect. Asking for
`start=20` does not return "results 21–30" — it returns *a different random sample*. Fetch
the same company twice with naive paging and 40–60% of the job IDs differ. Offsets of 1000
or more are rejected outright.

So a naive scrape of a large company returns **a sample, not the roster**. Measured on a
company with 880 open roles: naive paging returned 660 — a quarter of the roles silently
missing, with no indication anything was wrong.

**This Actor keeps sampling and measures its own coverage.** Splitting the requests into two
independent halves and comparing the overlap gives an estimate of the true total — the same
mark-and-recapture method used to count fish in a lake. It keeps going until the estimated
coverage reaches your target.

Every company row carries the result:

| Field | Meaning |
|---|---|
| `openJobs` | Roles actually collected |
| `estimatedTotal` | Estimated true number of open roles |
| `coverage` | Fraction collected (e.g. `1.0`) |
| `estimatedMissing` | Roles believed still unseen |
| `requestsUsed` | What it cost to get there |

Verified against that 880-role company: **880 collected, 880 estimated, coverage 1.0, in
452 requests.**

***

### Optional: what changed since last run

Closed roles vanish from LinkedIn and leave nothing behind, so "what disappeared" cannot be
reconstructed after the fact — only recorded. Turn on `trackChanges` and each run compares
against the last.

**This is only reported when the comparison is sound.** Two runs, both at full coverage,
sixty seconds apart, on that same 880-role company:

```
opened: 0   closed: 0   changeMarginOfError: 0
```

Which is the correct answer — nothing changed in a minute. An earlier version of this Actor
reported "170 opened, 159 closed" for exactly that case, because it was comparing two
different samples. It now refuses to answer unless coverage is high, and reports
`changeMarginOfError` so you can see how much of any number could be measurement noise.

Opened/closed come back as `null`, with a reason, when: it is the first run, coverage did not
reach the target, or you changed the filters between runs (which changes which roles were in
scope). Hiring pace additionally needs 7 days of observation.

**Cost of certainty:** full coverage of a 880-role company took ~450 requests and about 9
minutes. You are charged per job returned, not per request, so this costs you time rather
than money. Set `targetCoverage` lower (default `0.99`) if you would rather have speed.

***

### Input

```json
{
  "companies": [
    "https://www.linkedin.com/company/stripe/",
    "anthropicresearch",
    "1035"
  ],
  "maxJobsPerCompany": 1000,
  "location": "United States",
  "datePosted": "past-week",
  "workType": "remote"
}
```

- `companies` — company URL, slug, or numeric ID. Country subdomains (`uk.linkedin.com/...`)
  work too.
- `location`, `keywords`, `datePosted`, `workType`, `experience` — optional filters, applied
  on top of the company filter.
- `maxJobsPerCompany` — default 1000, max 5000. **Set it above the company's real number of
  open roles.** You are charged per job returned, not per request, so a generous ceiling
  costs nothing extra for a smaller company.
- `trackChanges` — off by default. See above for when it is worth turning on.

***

### Pricing

Pay per event. You are charged for what you receive.

| Event | Price |
|---|---|
| Run started | $0.01 |
| Per company | $0.01 |
| Per job | $0.0008 ($0.80 / 1,000) |

Job rows are priced the same as a plain listing scrape, because that is what they are.

Failed company lookups are **not** charged. A company with zero open roles still returns a
company row — "they are not hiring right now" is an answer, not a failure.

***

### How it works

Reads only LinkedIn's public, logged-out job listing pages — 10 roles per request. Because
each request returns a fresh sample rather than a fixed page, the Actor keeps requesting
until several rounds in a row bring nothing new, then stops. A 670-role company took about
93 requests this way.

The company ID is resolved from the public company page using **two independent markers**
that must agree. If they disagree, the Actor returns an error instead of a guess — returning
another company's jobs under your company's name would be worse than returning nothing.

#### The line this Actor does not cross

Logging in is a design decision, not a promise:

- Request headers are a **frozen object**. `Cookie` and `Authorization` cannot be attached
  at runtime.
- Input fields that look like credentials (`cookie`, `session`, `token`, `li_at`,
  `password`) are **rejected**.
- Both are covered by unit tests.

***

### Related Actors

| Actor | Use it when |
|---|---|
| [LinkedIn Jobs Scraper with Applicant Counts](https://apify.com/northbell/linkedin-jobs-applicants-scraper) | You want applicant counts and how fast they grow |
| [Fast LinkedIn Jobs Scraper](https://apify.com/northbell/linkedin-jobs-fast-scraper) | You want volume and speed, by search query |
| [LinkedIn Company Scraper with Headcount Growth](https://apify.com/northbell/linkedin-company-growth-scraper) | You want employee counts and growth |
| [LinkedIn Company Posts Scraper](https://apify.com/northbell/linkedin-company-posts-scraper) | You want what a company publishes and how it lands |

Pair this with the headcount scraper: **headcount tells you how big a company is, open roles
tell you where it is going next.**

# Actor input Schema

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

Company page URL, slug, or numeric ID. Country subdomains (uk.linkedin.com/...) work too.

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

Set this ABOVE the company's real number of open roles. LinkedIn does not offer stable deep paging, so the Actor keeps fetching until nothing new appears; if it stops at this ceiling instead, collection is incomplete and change tracking is withheld (opened/closed come back as null). You are charged per job returned, not per request, so a generous ceiling is free.

## `trackChanges` (type: `boolean`):

OFF by default. LinkedIn's public listing does not paginate reliably, so a company's roster can only be collected exactly when it is small - in practice about one page (10 roles), or a narrow filter. When ON, the Actor collects twice and only reports opened/closed if both passes match exactly; otherwise it reports null. Doubles the requests.

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

Narrow to roles matching a phrase, on top of the company filter.

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

City, region or country as LinkedIn spells it - "United States", "Berlin, Germany".

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

Narrowing to the last week is the usual choice for a weekly run.

## `workType` (type: `string`):

Filter to on-site, remote or hybrid roles.

## `experience` (type: `string`):

Filter by seniority as LinkedIn classifies it.

## `maxRequestsPerMinute` (type: `integer`):

Lower it if you run many companies at once.

## `targetCoverage` (type: `string`):

How completely to collect before stopping. The Actor estimates its own coverage by mark-and-recapture and keeps sampling until it reaches this. Higher costs more requests (time), not more money.

## `maxRequests` (type: `integer`):

Safety stop. A company with 880 open roles needed about 450 requests to reach full coverage.

## Actor input object example

```json
{
  "companies": [
    "https://www.linkedin.com/company/stripe/"
  ],
  "maxJobsPerCompany": 1000,
  "trackChanges": false,
  "datePosted": "any",
  "workType": "any",
  "experience": "any",
  "maxRequestsPerMinute": 30,
  "targetCoverage": "0.99",
  "maxRequests": 900
}
```

# Actor output Schema

## `companies` (type: `string`):

One row per company: how many roles are open, how many opened, how many disappeared, and the hiring pace.

## `jobs` (type: `string`):

One row per open role, flagged if it is new since your last run.

## `all` (type: `string`):

Every row: company, job and error.

# 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/stripe/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/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/stripe/"] }

# Run the Actor and wait for it to finish
run = client.actor("northbell/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/stripe/"
  ]
}' |
apify call northbell/linkedin-company-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,northbell/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/OEy7VipfzpVhBugZK/builds/Ji94MeX25IRx3Z5J7/openapi.json
