# LinkedIn Jobs Scraper by URL — Listings, Salaries (No Login) (`cryptosignals/linkedin-jobs-scraper`) Actor

Scrape LinkedIn job listings by search URL — extract job title, company, salary, location, skills and apply links. No login, no API key, no rate limits. Bypass anti-bot detection. CSV/JSON output. Pay per result. Proxycurl alternative for job board scraping and recruiter lead generation.

- **URL**: https://apify.com/cryptosignals/linkedin-jobs-scraper.md
- **Developed by:** [Web Data Labs](https://apify.com/cryptosignals) (community)
- **Categories:** Lead generation, AI
- **Features:** Limited permissions, Multi-tenant, Server interface
- **Stats:** 787 total users, 27 monthly users, 99.4% runs succeeded, 10 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.00 / 1,000 result scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Jobs Scraper — Public Job Listings (No Login Required)

> **Free plan?** Apify gives every user free monthly platform credits — enough to scrape plenty of job listings at this actor's per-result price. No credit card needed.

Scrape LinkedIn job listings at scale without login credentials or authentication. This actor uses LinkedIn's public guest API to search and extract job postings, returning structured data including job title, company, location, description, apply type, and apply links when LinkedIn exposes them.

**No login required** | **Full job details in search mode** | **Pay per result**

***

### Why Use This Actor?

LinkedIn is the world's largest professional job board with millions of active listings. But LinkedIn's interface makes it difficult to:

- Export job data in bulk
- Compare salaries across roles and locations
- Track job market trends over time
- Build pipelines of job leads for recruiting
- Feed job data into analytics dashboards

This actor solves all of that. You get clean, structured JSON data from LinkedIn's public job listings — no login, no cookies, no accounts to manage.

***

### Key Use Cases

#### 🔍 Job Market Research

Track hiring trends across industries. Monitor which skills are in demand, which companies are hiring, and how salaries change by region. Perfect for HR consultants, workforce analysts, and career coaches who need data-driven insights.

#### 📊 Recruitment Analytics

Build dashboards showing competitor hiring activity. Know when a competitor posts 50 new engineering roles — that's a signal. Recruiting firms use this to identify clients who are actively scaling.

#### 💼 Lead Generation

Every job posting is a buying signal. A company posting for a "DevOps Engineer" probably needs DevOps tools. A company hiring "SDRs" is scaling sales. Feed job listings into your CRM to identify warm leads based on hiring activity.

#### 🤖 Automated Job Alerts

Set up scheduled runs to monitor specific keywords and locations. Get notified when new roles matching your criteria appear. Build custom job boards or feeds for niche audiences.

#### 📈 Salary Benchmarking

Collect salary data across roles, locations, and experience levels. Build compensation reports for clients or internal planning. Compare your offers against market rates.

#### 🎓 Academic Research

Study labor market dynamics, remote work trends, skill demand evolution, or geographic hiring patterns. Export to CSV/Excel for use in research papers and reports.

***

### How It Works

The actor operates in two modes:

#### Mode 1: Search Jobs

Search LinkedIn's public job listings by keyword and location. Returns a list of matching jobs — by default with the same full detail fields as job mode (description, applicants, seniority, employment type, job function, industries, apply info). Detail fetching is time-budgeted: rows still waiting for their detail page when the run's remaining time runs out come back with the 6 summary fields at the same per-row price — a longer run timeout yields more detailed rows. Set `fetchDetails: false` for the lighter 6-field summary rows.

#### Mode 2: Get Job Details

Fetch the full details of a specific job posting by URL or job ID. Returns the complete job description, requirements, and metadata.

***

### Input Parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `action` | string | Yes | `search` | `"search"` to find jobs by keywords, `"job"` to get full details of a specific job |
| `keywords` | string | No | `"python developer"` | Search terms (e.g., `"data engineer"`, `"marketing manager remote"`) |
| `location` | string | No | `""` | Location filter (e.g., `"United States"`, `"London"`, `"Berlin"`) |
| `maxItems` | integer | No | `5` | Upper bound on results — batch accepts up to 1,000, the HTTP API interface caps at 100 per request. A ceiling, not a guarantee: LinkedIn's public search stops paginating well before 1,000 for most queries |
| `fetchDetails` | boolean | No | `true` | Fetch the full posting for every search result — slower (one extra page per job), the price per row is the same. Time-budgeted: rows that miss the run's remaining time fall back to the 6 summary fields at the same price. `false` returns only the 6-field summary rows |
| `datePosted` | string | No | `"any"` | Posting-age filter for search: `"any"`, `"past24h"`, `"pastWeek"`, `"pastMonth"` |
| `companyIds` | array | No | — | Restrict results to these LinkedIn company IDs — numeric strings like `"1068"` (JPMorganChase), up to 10. **Server-side filter**: it rides the search URL, rows keep the 6 summary fields |
| `easyApply` | boolean | No | `false` | LinkedIn's own **Easy Apply** filter — jobs you apply to on LinkedIn itself. Server-side, and when details are on each row is also checked: clearly-external applies are dropped, undetectable rows are kept |
| `under10Applicants` | boolean | No | `false` | LinkedIn's own **Under 10 applicants** filter — early postings only. It is LinkedIn's filter, not a recount: the guest page can show at most "first 25 applicants", so rows are only *dropped* when a numeric count over 10 is visible; unknown counts are kept |
| `experienceLevels` | array | No | — | Keep only these seniority levels: `Internship`, `Entry level`, `Associate`, `Mid-Senior level`, `Director`, `Executive`. **Client-side filter** — forces detail fetching and scans extra listings until `maxItems` match (bounded — see *Filtering*) |
| `jobTypes` | array | No | — | Keep only these employment types: `Full-time`, `Part-time`, `Contract`, `Temporary`, `Volunteer`, `Internship`, `Other`. **Client-side filter** — same scanning behavior as `experienceLevels`; combined, both must match |
| `url` | string | No | `""` | LinkedIn job URL or job ID, used with `action: "job"` |

#### Input Examples

**Search for remote Python jobs in the US (25 results):**

```json
{
  "action": "search",
  "keywords": "python developer remote",
  "location": "United States",
  "maxItems": 25
}
```

**Search for marketing roles in London:**

```json
{
  "action": "search",
  "keywords": "marketing manager",
  "location": "London",
  "maxItems": 50
}
```

**Get full details of a specific job:**

```json
{
  "action": "job",
  "url": "https://www.linkedin.com/jobs/view/1234567890"
}
```

**Broad search for data engineering jobs (highest allowed ceiling):**

```json
{
  "action": "search",
  "keywords": "data engineer",
  "maxItems": 1000
}
```

`maxItems` is an **upper bound, not a quota** — LinkedIn's public job search stops paginating well before 1,000 for most queries, so a large request usually returns fewer rows. When that happens the run's status message says so ("LinkedIn search ended after N results").

**Jobs posted in the last 24 hours, summary rows only:**

```json
{
  "action": "search",
  "keywords": "registered nurse",
  "location": "Chicago",
  "datePosted": "past24h",
  "fetchDetails": false
}
```

**Full-time mid-or-senior engineering roles at specific companies, Easy Apply only:**

```json
{
  "action": "search",
  "keywords": "software engineer",
  "location": "New York",
  "maxItems": 20,
  "companyIds": ["1068"],
  "easyApply": true,
  "experienceLevels": ["Mid-Senior level", "Associate"],
  "jobTypes": ["Full-time"]
}
```

***

### Filtering

Two kinds of filters, and they cost differently:

- **Server-side** — `companyIds`, `easyApply`, `under10Applicants`, `datePosted`, `location`, `keywords`. These ride the search URL, so LinkedIn does the filtering and the scan costs nothing extra.
- **Client-side** — `experienceLevels`, `jobTypes`, plus `easyApply`/`under10Applicants` verification when details are on. LinkedIn only exposes seniority and employment type on each job's detail page, so these filters fetch and check each listing until `maxItems` rows match — they force detail fetching on and scan extra listings.

What that means in practice:

- **Recall is bounded, not exhaustive.** A client-side filter keeps scanning until `maxItems` match, the scan cap is hit (up to 10× `maxItems` in batch, 3× `maxItems` capped at 50 on the HTTP interface), or the listings stop matching fast enough to keep going — whichever comes first. Most LinkedIn jobs mark seniority `Not Applicable`, so a narrow `experienceLevels` choice can scan many jobs before matching — and may still return fewer rows than asked, or the paid zero-match notice.
- **A zero-match filtered scan delivers one notice row** — "0 of N scanned jobs matched your filters" — charged as 1 result, exactly like a clean empty search. If every detail page failed instead, the run fails and nothing is charged.
- **Only matching rows are delivered and charged.** Rows the filter drops are never pushed and never billed.
- **Degraded rows don't count as matches.** A listing whose detail page couldn't be fetched has unknown seniority/type and is skipped by client-side filters — it is neither delivered nor charged.

***

### Sample Output

Each job listing returns a structured JSON object. Here's what a typical result looks like:

```json
{
  "jobId": "3876543210",
  "title": "Senior Python Developer",
  "company": "TechCorp Inc.",
  "location": "San Francisco, CA (Remote)",
  "postedDate": "2 weeks ago",
  "applicants": "127 applicants",
  "applicantsCount": 127,
  "isEarlyApplicant": false,
  "description": "We're looking for a Senior Python Developer to join our platform team...",
  "seniority": "Mid-Senior level",
  "employmentType": "Full-time",
  "jobFunction": "",
  "industries": "Software Development",
  "url": "https://www.linkedin.com/jobs/view/3876543210",
  "applyType": "EXTERNAL",
  "applyUrl": null,
  "salary": "$137,750.00/yr - $185,000.00/yr",
  "salaryMin": 137750.0,
  "salaryMax": 185000.0,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "salaryProvider": "TechCorp Inc. provided pay range",
  "companyUrl": "https://www.linkedin.com/company/techcorp",
  "companyLogoUrl": "https://media.licdn.com/dms/image/.../techcorp_logo",
  "companyId": "1068"
}
```

**Fields returned** (availability depends on what LinkedIn provides for each listing):

Search results (`action: "search"`) return the full field set by default — `fetchDetails` fetches each posting's detail page, within a time budget derived from the run's timeout. With `fetchDetails: false` — or whenever one detail page cannot be fetched, or whenever the detail budget runs out before that row's turn — the row falls back to the 6 summary fields at the same price:

- `jobId`, `title`, `company`, `location`, `postedDate` (ISO date), `url`

Job details (`action: "job"`) and detail-enriched search rows additionally return:

- `applicants` / `applicantsCount` / `isEarlyApplicant` — applicant caption and parsed count
- `description` — full job description text
- `seniority`, `employmentType`, `jobFunction`, `industries` — job criteria. **`jobFunction` is no longer sent on LinkedIn's public guest pages** — the key is kept for compatibility but arrives empty on current listings.
- `applyType` — `"EASY_APPLY"`, `"EXTERNAL"` (offsite company ATS), or `null` when the guest page shows no apply marker LinkedIn can be classified from
- `applyUrl` — **usually `null`**. LinkedIn does not expose the external apply URL on the public guest surface without login; this field is filled only when the guest HTML still includes it (opportunistic). Do not rely on it being present.
- `salary` — the raw salary text exactly as LinkedIn shows it, e.g. `"$137,750.00/yr - $185,000.00/yr"`; `null` when the employer doesn't disclose pay
- `salaryMin` / `salaryMax` — parsed numeric bounds (`"From $X"` → min only, `"Up to $X"` → max only, a single value → both)
- `salaryCurrency` — `USD`, `CAD`, `AUD`, `NZD`, `GBP`, `EUR`, `INR` or the ISO code LinkedIn used
- `salaryPeriod` — `"year"`, `"month"` or `"hour"`. An unrecognised period keeps `salary` text but leaves the numbers `null` — bounds are never invented
- `salaryProvider` — who published the range, e.g. `"Employer-provided"`
- `companyUrl` — the company's LinkedIn page (tracking parameters stripped); `null` when LinkedIn doesn't link it
- `companyLogoUrl` — the employer's logo image URL; `null` when absent
- `companyId` — the company's numeric LinkedIn ID — feed it straight back into `companyIds`; `null` when LinkedIn doesn't expose one

***

### Code Examples

#### Python — Search for Jobs and Save to CSV

```python
from apify_client import ApifyClient
import csv

client = ApifyClient("YOUR_APIFY_API_TOKEN")

run_input = {
    "action": "search",
    "keywords": "data scientist",
    "location": "New York",
    "maxItems": 50,
}

run = client.actor("cryptosignals/linkedin-jobs-scraper").call(run_input=run_input)

## Save results to CSV
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())

with open("linkedin_jobs.csv", "w", newline="", encoding="utf-8") as f:
    if items:
        writer = csv.DictWriter(f, fieldnames=items[0].keys())
        writer.writeheader()
        writer.writerows(items)

print(f"Saved {len(items)} jobs to linkedin_jobs.csv")
```

#### Python — Monitor New Jobs Daily

```python
from apify_client import ApifyClient
import json

client = ApifyClient("YOUR_APIFY_API_TOKEN")

## Search for recently posted jobs
run_input = {
    "action": "search",
    "keywords": "machine learning engineer",
    "location": "Remote",
    "maxItems": 100,
}

run = client.actor("cryptosignals/linkedin-jobs-scraper").call(run_input=run_input)

items = list(client.dataset(run["defaultDatasetId"]).iterate_items())

## Filter for jobs with salary info
jobs_with_salary = [job for job in items if job.get("salary")]
print(f"Found {len(jobs_with_salary)} jobs with salary data out of {len(items)} total")

for job in jobs_with_salary[:5]:
    print(f"  {job['title']} at {job['company']} — {job['salary']}")
```

#### Python — Get Full Details of a Specific Job

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_API_TOKEN")

run_input = {
    "action": "job",
    "url": "https://www.linkedin.com/jobs/view/3876543210",
}

run = client.actor("cryptosignals/linkedin-jobs-scraper").call(run_input=run_input)

items = list(client.dataset(run["defaultDatasetId"]).iterate_items())

if items:
    job = items[0]
    print(f"Title: {job['title']}")
    print(f"Company: {job['company']}")
    print(f"Description:\n{job.get('description', 'N/A')}")
```

#### Node.js — Search and Process Results

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

const client = new ApifyClient({ token: 'YOUR_APIFY_API_TOKEN' });

const input = {
    action: 'search',
    keywords: 'frontend developer react',
    location: 'United States',
    maxItems: 50,
};

const run = await client.actor('cryptosignals/linkedin-jobs-scraper').call(input);

const { items } = await client.dataset(run.defaultDatasetId).listItems();

console.log(`Found ${items.length} jobs`);

// Group by company
const byCompany = {};
for (const job of items) {
    const company = job.company || 'Unknown';
    byCompany[company] = (byCompany[company] || 0) + 1;
}

console.log('\nTop hiring companies:');
Object.entries(byCompany)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10)
    .forEach(([company, count]) => {
        console.log(`  ${company}: ${count} open positions`);
    });
```

#### Node.js — Get Job Details

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

const client = new ApifyClient({ token: 'YOUR_APIFY_API_TOKEN' });

const input = {
    action: 'job',
    url: 'https://www.linkedin.com/jobs/view/3876543210',
};

const run = await client.actor('cryptosignals/linkedin-jobs-scraper').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();

if (items.length > 0) {
    const job = items[0];
    console.log(`${job.title} at ${job.company}`);
    console.log(`Location: ${job.location}`);
    console.log(`Salary: ${job.salary || 'Not disclosed'}`);
    console.log(`Apply: ${job.applyUrl}`);
}
```

#### Using the Apify API Directly (cURL)

```bash
## Start a run
curl -X POST \
  "https://api.apify.com/v2/acts/cryptosignals~linkedin-jobs-scraper/runs?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "search",
    "keywords": "product manager",
    "location": "Berlin",
    "maxItems": 25
  }'

## Fetch results (use the defaultDatasetId from the run response)
curl "https://api.apify.com/v2/datasets/DATASET_ID/items?format=json&token=YOUR_TOKEN"
```

#### Call as an API (instant HTTP interface)

This actor also exposes a **request/response HTTP interface** — results come back in the response body, no run polling and no dataset step. Same input parameters, same per-result pricing.

```
GET/POST https://cryptosignals--linkedin-jobs-scraper.apify.actor
```

```bash
## Search jobs — GET with query params
curl "https://cryptosignals--linkedin-jobs-scraper.apify.actor/?action=search&keywords=devops&location=Remote&maxItems=5" \
  -H "Authorization: Bearer YOUR_APIFY_TOKEN"

## Or POST with a JSON body
curl -X POST "https://cryptosignals--linkedin-jobs-scraper.apify.actor/" \
  -H "Authorization: Bearer YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"search","keywords":"data engineer","maxItems":5}'

## Get one job posting
curl "https://cryptosignals--linkedin-jobs-scraper.apify.actor/?action=job&url=https://www.linkedin.com/jobs/view/1234567890" \
  -H "Authorization: Bearer YOUR_APIFY_TOKEN"
```

Response:

```json
{
  "items": [ { "jobId": "…", "title": "…", "company": "…", "location": "…", "url": "…" } ],
  "count": 5
}
```

Notes:

- **Authenticate with your Apify token** in the `Authorization: Bearer` header (or `?token=` query param), exactly like the Apify API.
- **Cold start:** if no server instance is warm, the first request may wait up to ~2 minutes while one starts. Warm requests answer in seconds.
- **`proxyConfiguration` is not supported** on this interface — requests containing it are rejected with HTTP 400.
- **`maxItems` is capped at 100 per request** on this interface — batch runs accept up to 1,000 as an upper bound (LinkedIn's public search ends earlier for most queries).
- **`fetchDetails` is opt-in here** — it defaults to off on this interface, so an unchanged call keeps the fast 6-field rows; pass `fetchDetails=true` for full postings inline. Detail fetches share the request's time budget — rows that miss it still return the 6 summary fields, at the same per-row price. Client-side filters (`experienceLevels`, `jobTypes`) force it on.
- **All five filters work in the query string**: `companyIds=1068,961661` (comma-separated), `easyApply=true`, `under10Applicants=true`, `experienceLevels=Director,Executive`, `jobTypes=Contract,Part-time` — or as arrays in a JSON POST body. An out-of-domain enum or a non-numeric company id is rejected with HTTP 400 before any work.
- **A filtered scan on this interface scans at most 3× `maxItems` listings (hard-capped at 50)** — a request can legitimately return fewer rows than asked. Rows already collected when the request deadline hits come back as a partial HTTP 200 with a `note` field — never thrown away.
- Common field aliases work too (`query`→`keywords`, `limit`→`maxItems`, `jobId`→`url`).
- You are charged the same per-result price, only for results actually delivered in the response.

***

### Pricing

This actor uses **Pay Per Event (PPE)** pricing — you only pay for the results delivered. The current price per result is shown live on the actor's Apify Store page.

| Event | How it's billed |
|-------|-----------------|
| **Result** | One charge per job row delivered — including the single no-results notice row a clean empty search returns |

**No platform fees.** A search that matches nothing still performs the lookup, so it delivers one notice row explaining the empty result and is charged as 1 result.

Free-plan Apify users get monthly platform credits which can be used alongside PPE actor costs — plenty for testing, small projects, or personal job searches.

> **Tip:** Start with `maxItems: 5` to test your search parameters before scaling up — a small run costs only a few results.

***

### Scheduling & Automation

Set up **scheduled runs** on Apify to automatically scrape new job listings on a daily or weekly basis:

1. Go to the actor's page on Apify Console
2. Click **"Schedule"** in the top menu
3. Set your desired frequency (daily, every 6 hours, weekly, etc.)
4. Configure your input parameters
5. Results are automatically saved to your dataset

Combine with **Apify Integrations** to push results directly to:

- Google Sheets
- Slack notifications
- Webhooks (your own API)
- Zapier / Make.com
- Email alerts

***

### Output Formats

Export your results in any format:

| Format | How |
|--------|-----|
| **JSON** | Default. Download from dataset or use API |
| **CSV** | Click "Export" → CSV in Apify Console |
| **Excel** | Click "Export" → Excel in Apify Console |
| **API** | Fetch programmatically via dataset API endpoint |

***

### Frequently Asked Questions

#### Do I need a LinkedIn account?

**No.** This actor uses LinkedIn's public guest API. No login, no cookies, no account required. It only accesses publicly available job listings — the same ones visible to anyone browsing LinkedIn without logging in.

#### Is this against LinkedIn's terms of service?

This actor accesses only publicly available data through LinkedIn's public endpoints. It does not bypass any authentication or access private data. Public job listings are intentionally made available by employers to reach the widest audience.

#### How many jobs can I scrape per run?

`maxItems` sets the ceiling — up to **1,000 per run** in batch, 100 per request on the HTTP standby/API interface. In practice the real limit is LinkedIn's public job search itself: it stops paginating well before 1,000 for most queries, so large requests usually return fewer rows — the run's status message reports when the search ended early. For larger volumes, set up multiple runs with different search parameters (different keywords, locations, or `datePosted` windows).

#### How fresh is the data?

The data is scraped in real-time from LinkedIn when you run the actor. You always get the latest listings. For monitoring new postings, schedule the actor to run daily or every few hours.

#### What if my search returns 0 results?

- Check your keyword spelling
- Try broader search terms
- Remove or change the location filter
- Narrow filters like `datePosted: "past24h"` or a restrictive `experienceLevels`/`jobTypes` choice legitimately hit this more often — fewer postings match within the bounded scan
- LinkedIn may not have listings matching very niche queries
- **A clean empty search — and a filtered scan that matched nothing — delivers one notice row and is charged as 1 result** — the lookup itself is the work

#### Can I filter by remote/hybrid/on-site?

Yes — include terms like "remote" in your keywords. For example: `"software engineer remote"`.

#### Can I get salary data?

Salary information is returned **when the employer includes it** in the listing. Not all job postings disclose salary. The actor returns whatever LinkedIn provides.

#### How is this different from other LinkedIn scrapers?

- **No login required** — many competitors require LinkedIn credentials (which risk account bans)
- **Public API only** — we don't scrape HTML or use headless browsers, so it's fast and reliable
- **Simple pricing** — pay per result, no hidden fees
- **Full details in bulk** — search fetches each posting's detail page by default, bounded by the run's time budget
- **Two modes** — search for jobs OR get full details of a specific listing

#### Can I use this for commercial purposes?

Yes. The data is publicly available. You can use it for analytics, lead generation, research, and building products. Always ensure your use case complies with applicable data protection laws in your jurisdiction.

#### What happens if LinkedIn changes their API?

We actively maintain this actor. If LinkedIn makes changes, we update the actor to ensure continued functionality.

***

### Rate Limits & Best Practices

- **Start small**: Test with `maxItems: 5` before scaling up
- **Be specific**: Precise keywords return better results than broad ones
- **Use location**: Adding a location filter reduces noise and speeds up results
- **Schedule smart**: Daily runs catch most new postings; hourly is rarely needed
- **Combine searches**: Run multiple searches with different keywords to build comprehensive datasets

***

### Support

Having issues or questions? Use the **Issues** tab on this actor's Apify page to report bugs or ask questions. We typically respond within 24 hours.

***

*Built and maintained by [cryptosignals](https://apify.com/cryptosignals).*

***

*Found this actor useful? [Leave a review](https://apify.com/cryptosignals/linkedin-jobs-scraper#reviews) - it helps a lot. Something broken? Report it on the [Issues tab](https://apify.com/cryptosignals/linkedin-jobs-scraper/issues).*

# Actor input Schema

## `action` (type: `string`):

What to do: 'search' to find jobs by keywords, 'job' to get full details of a specific job.

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

Search keywords (e.g. 'python developer', 'data engineer'). Used with action=search.

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

Location filter (e.g. 'United States', 'London', 'Remote'). Used with action=search.

## `maxItems` (type: `integer`):

Upper bound on job listings to return — up to 1,000 per run here; the HTTP standby interface caps at 100 per request. A ceiling, not a guarantee: LinkedIn's public job search stops paginating well before 1,000 for most queries, so large values usually return fewer rows. When that happens the run's status message says the search ended early.

## `fetchDetails` (type: `boolean`):

When enabled (the default), search mode fetches the full posting for every result — description, applicants, seniority, employment type, job function, industries and apply info — the same fields action=job returns. It is slower because one extra page is fetched per job; the price per row is the same either way. Detail fetching is time-budgeted: rows still waiting for their detail page when the run's remaining time runs out come back with the 6 base fields at the same price — a longer run timeout yields more detailed rows. Set to false to get only the 6-field summary rows.

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

Filter search results by posting age. Used with action=search. Note: 'past24h' can legitimately return the paid no-results notice more often — fewer postings match.

## `companyIds` (type: `array`):

Restrict results to these LinkedIn company IDs (numeric strings, e.g. "1068" for JPMorganChase — find it in the company's LinkedIn page URL or via our actor's companyId output field). Server-side filter, up to 10 IDs. Used with action=search.

## `experienceLevels` (type: `array`):

Keep only jobs whose seniority matches one of these values. Client-side filter: it requires full details (forces fetchDetails on) and scans extra listings — most LinkedIn jobs mark seniority 'Not Applicable', so a narrow choice can scan many jobs before matching. Several values are OR-ed; combined with Job Types both must match.

## `jobTypes` (type: `array`):

Keep only jobs whose employment type matches one of these values. Client-side filter: it requires full details (forces fetchDetails on) and scans extra listings until maxItems match (bounded — see README). Several values are OR-ed; combined with Experience Levels both must match.

## `easyApply` (type: `boolean`):

LinkedIn's own 'Easy Apply' filter — jobs you apply to on LinkedIn itself. With detail fetching on, each row is additionally checked client-side and clearly-external applies are dropped; undetectable rows are kept.

## `under10Applicants` (type: `boolean`):

LinkedIn's own 'Under 10 applicants' filter — early postings only. With detail fetching on, rows are additionally checked client-side and jobs with a visible count over 10 are dropped.

## `url` (type: `string`):

Full LinkedIn job URL or job ID. Used with action=job. Example: https://www.linkedin.com/jobs/view/1234567890

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

Optional proxy settings. Use Apify Proxy or a custom proxy to avoid rate limiting.

## Actor input object example

```json
{
  "action": "search",
  "keywords": "python developer",
  "location": "",
  "maxItems": 5,
  "fetchDetails": true,
  "datePosted": "any",
  "easyApply": false,
  "under10Applicants": false,
  "url": "",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (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 = {
    "action": "search",
    "keywords": "python developer",
    "location": "",
    "maxItems": 5,
    "fetchDetails": true,
    "datePosted": "any",
    "url": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("cryptosignals/linkedin-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 = {
    "action": "search",
    "keywords": "python developer",
    "location": "",
    "maxItems": 5,
    "fetchDetails": True,
    "datePosted": "any",
    "url": "",
}

# Run the Actor and wait for it to finish
run = client.actor("cryptosignals/linkedin-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 '{
  "action": "search",
  "keywords": "python developer",
  "location": "",
  "maxItems": 5,
  "fetchDetails": true,
  "datePosted": "any",
  "url": ""
}' |
apify call cryptosignals/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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