# LinkedIn Jobs Scraper & Search API: Apply Links, No Reposts (`sharplinelabs/linkedin-jobs-search`) Actor

Search an hourly index of public LinkedIn jobs. Drop reposts and agencies, or find hard-to-fill roles as hiring leads for staffing agencies. Filter by remote/hybrid, experience, salary, visa, industry and company size. Every job has an apply link, the employer's own where we find it. No login.

- **URL**: https://apify.com/sharplinelabs/linkedin-jobs-search.md
- **Developed by:** [SharpLine Labs](https://apify.com/sharplinelabs) (community)
- **Categories:** Jobs, Lead generation, AI
- **Stats:** 4 total users, 3 monthly users, 100.0% runs succeeded, 3 bookmarks
- **User rating**: 5.00 out of 5 stars

## 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?

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

### What is LinkedIn Jobs Search Pro?

**LinkedIn Jobs Search Pro searches an hourly index of public LinkedIn jobs and adds the employer's own apply link (Workday, Greenhouse, iCIMS and other careers sites) wherever we can find it.** Write your search the way you think about it (`python AND (django OR flask) NOT php`, `^Senior`, `=Data Engineer`), choose countries and a time range, and get clean JSON, CSV or Excel. There are also token-lean YAML, Markdown and TOON exports for AI agents.

No LinkedIn account, no cookies, no login. Pay only for the jobs you get: **$1.25 per 1,000 jobs**, no start fee.

**Try it:** keep the prefilled input (US, past 24 hours, data engineer jobs, 20 results) and press **Start**.

### Why use this LinkedIn jobs scraper?

- **Every job has an apply link, labelled.** For jobs that send you off LinkedIn, we look the job up on the employer's own careers board (found for **~27% of off-site jobs overall and ~36% in the US** as of 2026-10-26). By default a partner lookup fills the rest, and anything still missing links to the LinkedIn job page. Every link says where it came from: `easy_apply`, `board_verified`, `board_title` (probable), `partner` or `linkedin`.
- **Title search that does what you mean.** Type `data engineer` and you get Data Engineers, not "Data Center Engineer". Need more? Every text field also takes `prefix*`, `^starts-with`, `=exact`, `AND`/`OR`/`NOT` with parentheses and `/regex/`. `C++`, `C#` and `.NET` work as written.
- **Fresh index.** Refreshed every hour since 2026-10-24, and the busiest areas every 30 minutes: the large US states (California, Texas, New York, Florida, Illinois, Pennsylvania, Ohio, North Carolina, Virginia, Washington), Canada, the United Kingdom, Germany, France, India, Brazil, China and Japan.
- **Coverage notes, published.** LinkedIn shows at most about 1,000 results per search. When an area hits that cap, our crawler splits it into regions and searches each one. At peak hours some loss can remain; we estimate and track it on every run, and flag areas where it can't be measured.
- **Remote / hybrid / on-site from the job's own text.** We read the location, LinkedIn's tag and the description. When there's no signal the job says `unknown` rather than guessing. Accuracy numbers will be published once our 300-job labelled set is done.
- **No reposts, no agencies (if you want).** LinkedIn shows a re-listed job as "just posted"; we spot these by the job ID and drop them by default (they are often half of the "new" jobs). One switch drops staffing firms, recruiters and job boards. The same job posted in several cities comes back once, with all its `locations`.
- **Hiring leads for agencies.** Every job carries its real age (`job_age_days`, from the job ID) and `hard_to_fill`: re-listed and open 30+ days. `hardToFill: "only"` returns just those roles, and the `HIRING_LEADS` record groups each run by employer (open roles, hard-to-fill roles, company size, industry, website). About 1 in 7 of the US jobs LinkedIn shows as new are hard-to-fill roles.
- **Built for schedules.** `onlyNewSinceLastRun` returns only jobs this exact search hasn't delivered before, and never charges twice for a job it already checked.
- **Built for AI agents.** A compact `ai` view and YAML/Markdown/TOON exports keep the full description while dropping HTML and noise. Callable from Claude, ChatGPT, Cursor and other agents through Apify's MCP server.
- **Honest failure modes.** A filter rule that can't be parsed, or a time range our history doesn't cover yet, stops the run with a clear message and **costs nothing**.

### How to search LinkedIn jobs: step by step

1. Pick a **time range** (past hour or past 24 hours today; longer ranges open as our history grows, see below).
2. Pick one or more **countries**. The default is the United States; clear the list (an empty `countries: []`) to search all indexed countries.
3. Type the **job titles** you want, one per line, and any titles to exclude. Optionally add **locations**.
4. Narrow with **workplace**, **experience level** and **employment type**; tick **Exclude recruitment agencies** if you only want direct employers. Hard-to-fill roles (for agencies), company, description, salary, years of experience, visa sponsorship, industry, applicants, Easy Apply and ATS filters are under **More filters**.
5. Set **Max results** and press **Start**. Download the results as JSON, CSV, Excel, XML, HTML, JSONL or RSS, or call the API.
6. For alerts, save the input as a **task**, turn on **Only new since last run**, and add a **schedule**.

### Use cases

#### Job seekers and job-alert bots

Get the new remote data-engineering jobs in the US each hour, without the ones that are really on-site, and with the employer link so you can apply on the company's own site.

```json
{
  "timeRange": "1h",
  "countries": ["US"],
  "titleSearch": ["\"data engineer\"", "\"analytics engineer\""],
  "titleExclusionSearch": ["intern*", "^Lead", "director"],
  "workplace": ["remote"],
  "excludeReposts": true,
  "onlyNewSinceLastRun": true,
  "limit": 100
}
```

#### Staffing and recruitment agencies: hiring leads

Your best prospects are direct employers who have been trying to fill a role for weeks. LinkedIn hides that: a role re-listed for the third time shows as "posted 2 hours ago". This search finds those roles by job-ID age, drops other agencies, and hands you one row per company.

```json
{
  "timeRange": "24h",
  "countries": ["US"],
  "titleSearch": ["\"registered nurse\"", "\"data engineer\"", "\"electrician\""],
  "locationSearch": ["Texas"],
  "hardToFill": "only",
  "hardToFillDays": 30,
  "excludeAgencies": true,
  "onlyNewSinceLastRun": true,
  "limit": 300
}
```

What you get:

- **Jobs** (dataset, `hard_to_fill` view): title, location, `job_age_days` (how long the opening has really existed), company size, industry, website, LinkedIn link and the employer's own apply link.
- **`HIRING_LEADS`** (key-value record): one row per employer, sorted by hard-to-fill roles, then open roles: `open_roles`, `hard_to_fill_roles`, `oldest_role_age_days`, `titles`, `locations`, `company_size`, `company_industry`, `company_website`, `company_headquarters` and up to 10 example jobs. Agencies are left out.

Schedule it daily with `onlyNewSinceLastRun` for a fresh lead list every morning. Notes: a re-listed role is usually still open and often hard to fill, but some employers refresh many postings at once (one employer re-listed 25 roles in a single day, all 77 days old), so check `open_roles` against `hard_to_fill_roles`. Ages come from the job ID: exact to the day for IDs since Oct 2026, within a few weeks before that. For a broad search (no titles), raise `maxJobsToCheck`: candidates are capped at the newest 5 × `maxJobsToCheck` jobs before the hard-to-fill filter.

#### Recruiters and sourcers

Find fresh, low-competition roles at the companies you recruit for, or find which companies are struggling to fill a role (many applicants, reposted).

```json
{
  "timeRange": "24h",
  "countries": ["GB", "IE"],
  "titleSearch": ["^Senior AND (\"site reliability\" OR devops OR platform)"],
  "excludeAgencies": true,
  "seniority": ["mid-senior level"],
  "maxApplicants": 25,
  "limit": 200
}
```

#### Sales and lead generation: hiring signals

A company hiring a "Salesforce administrator" or a "head of RevOps" is a warm lead for CRM consultancies. Index-only mode returns title, company, location and the job link at the cheaper rate.

```json
{
  "timeRange": "24h",
  "countries": ["US", "CA"],
  "titleSearch": ["\"salesforce admin*\"", "\"revenue operations\"", "=RevOps Manager"],
  "includeDetails": false,
  "excludeReposts": true,
  "onlyNewSinceLastRun": true,
  "limit": 1000
}
```

#### HR-tech, ATS vendors and job-board aggregation

Feed a job board only with jobs whose employer link is confirmed, on the ATS platforms you support.

```json
{
  "timeRange": "24h",
  "countries": ["US"],
  "titleSearch": ["engineer*", "developer*"],
  "applyLinks": "verified",
  "ats": ["greenhouse", "lever", "ashby", "workday"],
  "easyApply": "exclude",
  "limit": 500
}
```

#### Labour-market research

Count today's postings for a role family across countries. Index-only keeps it cheap; `first_seen_at` gives you a clean timeline.

```json
{
  "timeRange": "24h",
  "countries": ["DE", "FR", "NL", "ES", "IT"],
  "titleSearch": ["/data\\s?(engineer|scientist|analyst)/"],
  "includeDetails": false,
  "limit": 5000
}
```

#### AI agents and LLM pipelines

Ask an agent to "find 50 remote Python jobs posted in the last hour and summarise them". The TOON export keeps the context small while every job keeps its full description.

```json
{
  "timeRange": "1h",
  "countries": ["US", "GB", "CA"],
  "titleSearch": ["python"],
  "workplace": ["remote"],
  "aiFormat": "toon",
  "limit": 50
}
```

#### Career coaches and university careers services

Build a weekly digest of graduate and entry-level roles for your students, with salary shown.

```json
{
  "timeRange": "24h",
  "countries": ["GB"],
  "titleSearch": ["graduate", "\"entry level\"", "junior", "trainee"],
  "seniority": ["internship", "entry level"],
  "hasSalary": true,
  "aiFormat": "markdown",
  "onlyNewSinceLastRun": true,
  "limit": 300
}
```

#### Relocation and visa-sponsorship search

Find jobs that say they sponsor visas (`"visaSponsorship": "not_no"` instead keeps every job except the ones that say they don't). Each job carries `visa_sponsorship`: yes, no or unknown.

```json
{
  "timeRange": "24h",
  "countries": ["NL", "DE", "IE", "AE", "SG"],
  "titleSearch": ["\"software engineer\"", "\"backend developer\""],
  "visaSponsorship": "yes",
  "limit": 200
}
```

### LinkedIn jobs boolean search: filter cheat-sheet

Every rule is case- and accent-insensitive. `AND`, `OR`, `NOT` must be UPPERCASE. One rule per line.

| You write | It matches | Example match |
|---|---|---|
| `java` | the whole word | "Senior **Java** Developer" (not "JavaScript") |
| `data engineer` | the exact phrase (plain words) | "Senior **Data Engineer**" (not "Data Center Engineer") |
| `"machine learning"` | the exact phrase (quotes optional) | "**Machine Learning** Engineer" |
| `python AND django` | both words, anywhere | "Django + **Python** dev" |
| `dev*` | words starting with | "**Dev**Ops", "**Developer**" |
| `*script` | words ending with | "Java**Script**", "Type**Script**" |
| `^Senior` | the field starts with | "**Senior** Data Analyst" |
| `=Data Engineer` | the whole field equals | "Data Engineer" only |
| `python AND (django OR flask) NOT php` | boolean logic | Python + Django, no PHP |
| `"C++"`, `C#`, `.NET` | kept as written | "**C++** Engineer", "**.NET** Developer" |
| `/data\s?eng/` | a regular expression (see below) | "Data Eng…", "DataEng…" |

Regular expressions go between slashes. Alternatives with `|` work too; written outside the table:

```
/data\s?(eng|sci)/
```

matches "Data Engineer", "DataScience" and "Data Scientist". Accents are ignored here as well: `/résumé/` matches "Résumé" and "resume".

- **Include rules** (`titleSearch`, `companySearch`, `locationSearch`, `descriptionSearch`): a job is kept when any line matches. To require several things, put them on one line with `AND`.
- **Exclusion rules** (`…ExclusionSearch`): a job is dropped if any rule matches.
- **Bad rule = free stop.** A rule that can't be parsed stops the run before anything is charged, naming the field and line (e.g. `titleSearch[0]: unbalanced '(' …`).
- **Cheap filters first.** Countries, time range, title, company, location, Easy Apply, reposts and the agency list run on the index and are free. Workplace, seniority, employment type, description, salary, years of experience, visa sponsorship, industry, applicants, ATS and the agency wording check need the job page and may add a `job-checked` charge for jobs they drop.

Other filters: `workplace` (remote / hybrid / on-site / unknown), `seniority`, `employmentType`, `excludeAgencies`, `excludeReposts` (on by default), `applyLinks` (complete / basic / verified), `hasSalary`, `maxApplicants`, `easyApply` (any / only / exclude), `ats`, and:

- `minSalary` + `salaryCurrency` (default USD): keeps jobs whose pay, converted to a year (hour × 2080, day × 260, week × 52, month × 12), reaches `minSalary` at the top of the range. Pay comes from LinkedIn's salary box, or else a pay range in the description ("$120k–$150k", "€55.000 – €65.000 brutto pro Jahr", "£40,000 per annum"); bonuses, sign-on amounts, 401(k) and funding figures are ignored. **When `minSalary` is set, jobs with no stated pay, or pay in another currency, are dropped** (no currency conversion).
- `experienceYears`: `0-2`, `3-5`, `6-10`, `10+` (inclusive, so 10 is in both of the last two) and `unknown` for jobs that don't state a number. Read from phrases like "3+ years", "minimum of 5 years", "5–7 years of experience", "mindestens 3 Jahre Berufserfahrung"; when a post lists several, the first required one counts.
- `visaSponsorship`: `any` (default), `yes` (the post says it sponsors) or `not_no` (drops only posts that say they don't sponsor, e.g. "unable to sponsor", "must be authorized to work in the US without sponsorship", "U.S. citizenship is required"). Most posts say neither and come out `unknown`.
- `industries` / `industryExclusion`: rules (same syntax as titles) against LinkedIn's industry for the job, e.g. `"staffing and recruiting"`. A job without an industry fails `industries` and passes `industryExclusion`. Older inputs (`titleMatchMode` and the other match modes, `applyLinkRequired`, `partnerApplyLinks`, `partnerIncludeProbable`) still work.

### Reposts and recruitment agencies

**Reposts.** LinkedIn job IDs grow over time, and a re-listed or refreshed job keeps its old ID while LinkedIn shows it as "posted 10 minutes ago". We record the newest job ID every hour, so we know how old an ID is. A job whose ID already existed **2 or more days before it appeared** is a repost (`is_repost: true`). In late October 2026 that was about half of the US jobs LinkedIn listed as new. `excludeReposts` (on by default) drops them; turn it off to see them too.

**Real age and hard-to-fill roles.** Every job also carries `id_created_estimate` (when its job ID was created) and `job_age_days`. A repost whose ID is at least `hardToFillDays` old (default 30) has `hard_to_fill: true`: the same opening re-listed for a month or more. `hardToFill: "only"` keeps just those (and keeps reposts whatever `excludeReposts` says). Of the US reposts on 2026-10-27, 30% were more than a month old and 14% more than three months.

**Agencies.** `excludeAgencies` (off by default) drops jobs from staffing firms, recruiters, job boards and gig platforms. Each job carries `is_agency` and `agency_reason`:

| `agency_reason` | Signal |
|---|---|
| `known` | on our list of large agencies, job boards and gig platforms (Robert Half, Insight Global, CyberCoders, Jobot, AMN, SonicJobs...) |
| `name` | the company name says so (Staffing, Recruiting, Personnel, Talent Partners...) |
| `title` | contract-staffing markers in the title (W2, C2C, `::`, "Local to") |
| `industry` | LinkedIn industry Staffing and Recruiting or Executive Search |
| `description` | third-party wording: "our client", "direct client", C2C, W2, "Visa:" |
| `contract_shop` | a contract job in IT consulting whose post opens with a staffing header ("Duration:", "long term contract") |

Employer notes such as "we don't accept resumes from agencies" don't count. Without job details (`includeDetails: false`) only the first three signals are used.

### Enriched fields (pattern-based, no AI guesswork)

Every job with details also carries fields we extract with code from the job page, the location and the company's public LinkedIn page. No language model: the same text always gives the same answer, and a field stays empty when the post doesn't say.

| Field | What it holds |
|---|---|
| `salary_min` / `salary_max` / `salary_currency` / `salary_period`, `salary_annual_min` / `_max` | pay from LinkedIn's salary block, else a pay range in the description |
| `experience_years_min`, `experience_level` | minimum years asked ("3+ years"), and the bucket `0-2` / `2-5` / `5-10` / `10+` |
| `visa_sponsorship` | `yes` / `no` / `unknown` |
| `work_arrangement`, `office_days`, `remote_scope` | `on_site` / `hybrid` / `remote_ok` / `remote_only`, office days per week, where a remote worker may live |
| `employment_type_normalized`, `contract_months` | FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM, OTHER |
| `categories` | up to 3 of 33 job categories (Technology, Healthcare, Finance & Accounting, ...) |
| `skills` | tools, languages, licenses and methods named in the post (from a 1,800-entry dictionary) |
| `education_min`, `education_preferred`, `degree_or_equivalent`, `certifications` | the lowest degree required, and named certifications (CPA, PMP, RN, ...) |
| `benefits` | health, dental, 401k/pension, PTO, parental leave, equity, bonus, ... |
| `hours_per_week`, `schedule` | stated hours, day/night/weekend/on-call shifts |
| `language`, `languages` | the description's language(s) |
| `summary`, `responsibilities`, `requirements` | sentences and bullets copied from the post's own sections (extractive, never rewritten) |
| `contact_emails` | email addresses in the post |
| `location_city` / `_region` / `_region_code` / `_country` / `_timezone`, `location_derived` | the location string, parsed |
| `company_size`, `company_employees_min` / `_max`, `company_employees_on_linkedin`, `company_industry`, `company_website`, `company_headquarters`, `company_type`, `company_founded`, `company_followers`, `company_specialties`, `company_logo` | from the company's public LinkedIn page (`includeCompanyDetails`, on by default) |

Filters on them (under **More filters**): `minSalary`, `experienceYears`, `visaSponsorship`, `workArrangement`, `categories` / `categoryExclusion`, `languages`, `companySize`, `industries` / `industryExclusion`. The **Enriched** dataset view shows the main ones as a table.

**Moving from Fantastic.jobs?** Set `compatibility: "fantastic"` and every job also gets the same data under their column names (`ai_salary_min_value`, `ai_experience_level`, `ai_work_arrangement`, `ai_taxonomies_a`, `ai_key_skills`, `org_linkedin_size`, `locations_derived`, ...), so existing scripts keep working. One difference: `ai_core_responsibilities` and `ai_requirements_summary` hold sentences copied from the post, not written summaries.

### Employer apply links: how they work

LinkedIn shows "Apply" either as **Easy Apply** (you apply inside LinkedIn, ~20% of jobs) or as an **off-site** link to the employer. For off-site jobs we look the job up on the employer's own careers board (Workday, SuccessFactors, iCIMS, Greenhouse, Lever, Ashby, Oracle and more) and label the result:

| `apply_source` | Meaning | `apply_confidence` |
|---|---|---|
| `easy_apply` | LinkedIn Easy Apply; `apply_url` is the LinkedIn job page | n/a |
| `board_verified` | Found on the employer's board with a matching requisition ID, or an identical title in the same city with a single candidate | `high` |
| `board_title` | **Probable**: a very close title, no location conflict, not confirmed. **Check before you apply.** | `medium` / `low` |
| `partner` | Filled by the partner lookup (Complete mode, the default; see below) | `high` |
| `linkedin` | No employer link found: `apply_url` is the LinkedIn job page (apply from there); `apply_reason` says why (`no_board_known`, `not_on_board`) | n/a |

`ats` names the platform of the link (e.g. `workday`, `greenhouse`). **Apply links** (`applyLinks`) has three modes: `complete` (default: our links, the partner fills the rest), `basic` (our links only, no partner cost) and `verified` (keep only Easy Apply and `board_verified` jobs).

#### Coverage today (employer off-site jobs, 2026-10-26)

| Country | Found | Country | Found | Country | Found |
|---|---|---|---|---|---|
| United States | 36% | Germany | 8% | Singapore | 24% |
| Canada | 25% | Spain | 12% | Japan | 19% |
| United Kingdom | 17% | Netherlands | 8% | Australia | 12% |
| Ireland | 30% | Italy | 12% | United Arab Emirates | 26% |
| France | 9% | India | 21% | China / Hong Kong | 18% |

Overall: ~27%. Coverage grows as we map more employers' boards. The LinkedIn `url` is always included.

#### Complete mode: missing links filled by a partner actor (your account, small cost)

In **Complete** mode (the default), off-site jobs we couldn't match, plus probable `board_title` ones, are sent (up to `partnerMaxJobs`, default 100) to **[bebity/linkedin-jobs-scraper](https://apify.com/bebity/linkedin-jobs-scraper)**, a separate third-party actor. Choose **Basic** to skip it.

- It runs **as a separate run on YOUR Apify account** and appears in your Console as its own run.
- **That actor bills you at its own price**, **$1 to $1.50 per 1,000 jobs it looks up**, depending on your Apify plan. We cap its run at a matching maximum cost and keep its paid company add-ons off.
- On top of that we charge `apply-link-partner`, **$0.20 per 1,000 links it actually fills**. Easy Apply answers are free.
- It may not work on Apify's **Creator plan**, which can't run other developers' actors. If the partner run fails, isn't allowed on your plan or there isn't enough run time left, your run still succeeds; `OUTPUT.partner` explains what happened, and there's no partner fee.

### Exports and AI-friendly output

**Dataset formats (built in):** JSON, CSV, Excel, XML, HTML, JSONL and RSS. Use the Export button or the API (`?format=csv`).

**Dataset views:** `overview` (the main table), `apply` (apply-link audit) and `ai` (compact fields for LLMs: title, company, location, posted date, workplace, seniority, employment type, salary, apply link, LinkedIn URL, full description).

**Token-lean exports for LLMs:** set `aiFormat` to `yaml`, `markdown` or `toon`. After the run, one record `JOBS_AI` in the run's key-value store holds all delivered jobs in that format. Empty fields and the HTML are dropped; the description stays whole unless you set `descriptionMaxChars`. `OUTPUT.ai_export_url` links to it. Like any record in a run's default key-value store, **that link can be read by anyone who has it**, without a token, so share it only where that's fine.

**Keep the full description.** It is what an AI needs most to judge a job (requirements, location rules, visa,
pay), so every export keeps it whole unless you set `descriptionMaxChars`. The format is where the savings are.
Measured on 100 real jobs with the `o200k_base` tokenizer (GPT-4o family; Claude's is similar), same fields, full description:

| Format | Tokens per job | vs JSON |
|---|---:|---:|
| JSON (minified, AI fields) | 879 | 100% |
| CSV | 853 | 97% |
| **TOON** | **854** | **97%** |
| Markdown | 882 | 100% |
| YAML | 888 | 101% |
| Full dataset item (all fields, text + HTML) | 2,155 | 245% |

The biggest saving is dropping `description_html` and the other non-AI fields (the `ai` view and `JOBS_AI` do this): about
60% fewer tokens with no loss of meaning. Between formats the difference is small; TOON and CSV are about 3-15% smaller
than JSON depending on how much of the job is description.

**Which to use:** for an agent or tool call, the `ai` dataset view as JSON. To paste many jobs into one prompt, `aiFormat: "toon"`.
For a human-readable digest or chat answer, `aiFormat: "markdown"`. TOON values that contain commas or quotes are
double-quoted CSV-style (inner quotes doubled). Set `descriptionMaxChars` only when a hard token budget forces it.

### How much does it cost to scrape LinkedIn jobs?

Pay per event. Platform usage is included, there's **no start fee**, and each job is charged at most once per run.

| Event | When | Price per 1,000 |
|---|---|---|
| `job-result` | A job delivered with details (`includeDetails` on, default) | **$1.25** |
| `job-index-only` | A job delivered with index fields only (`includeDetails` off) | **$0.40** |
| `job-checked` | A job page we opened for a detail filter, which the filter then dropped | **$0.20** |
| `apply-link-partner` | Complete mode only: a link the partner actually filled | $0.20 (+ the partner actor's own price on your account) |
| Bad filter rule / time range not available yet | n/a | **$0** |

**Worked examples**

| Scenario | Calculation | Cost |
|---|---|---|
| Try it: 20 jobs with details, Basic links | 20 × $0.00125 | **$0.025** |
| Try it: 20 jobs, Complete links (default), ~10 looked up by the partner | $0.025 + up to 10 × $0.0002 to us, plus ~$0.015 billed by the partner actor | **≈ $0.04** |
| 100 US jobs with details | 100 × $0.00125 | **$0.125** |
| 100 remote jobs, where 300 more jobs were opened and dropped by the workplace filter | 100 × $0.00125 + 300 × $0.0002 | **$0.185** |
| 1,000 jobs, index fields only (lead lists) | 1,000 × $0.0004 | **$0.40** |
| Hourly alert, ~20 new jobs per hour for 30 days | 14,400 × $0.00125 | **$18.00** |
| 100 jobs + partner lookup for 40 jobs without a link, 30 filled | $0.125 + 30 × $0.0002 to us, plus ~$0.04–0.06 billed by the partner actor | **≈ $0.17–0.19** |

Set **Maximum cost per run** in the run options; the run stops cleanly when it's reached. A 100-job US search took about 3 to 4 minutes in our tests (200 s in the latest one).

#### Is there a free LinkedIn jobs scraper?

Apify's free plan includes monthly platform credit you can spend on this actor. At $1.25 per 1,000 jobs, the try-it run above costs about $0.03.

### Time ranges

Our index history starts on **2026-10-24**. Longer ranges open as the history grows:

| Time range | Available from |
|---|---|
| Past hour | now |
| Past 24 hours | now |
| Past 7 days | 2026-10-31 |
| Past 30 days | 2026-11-23 |
| Past 3 months | 2027-01-24 |

Choosing a range early ends the run successfully with no charge, and the status message suggests what to use instead.

### Output example

```json
{
  "job_id": 4391715720,
  "url": "https://www.linkedin.com/jobs/view/4391715720",
  "title": "Lead Mechanical Engineer (Mission Critical/Data Centers)",
  "company": "WSP in the U.S.",
  "location": "Austin, TX",
  "locations": ["Austin, TX"],
  "country": "US",
  "posted_date": "2026-10-27",
  "first_seen_at": "2026-10-27T13:38:20+00:00",
  "is_repost": false,
  "id_created_estimate": "2026-10-27",
  "job_age_days": 0.6,
  "hard_to_fill": false,
  "is_agency": false,
  "agency_reason": null,
  "apply_type": "external",
  "apply_url": "https://emit.fa.ca3.oraclecloud.com/hcmUI/CandidateExperience/en/sites/CX_2001/job/83796",
  "apply_source": "board_verified",
  "apply_confidence": "high",
  "ats": "oracle_orc",
  "workplace": "unknown",
  "seniority": "Not Applicable",
  "employment_type": "Full-time",
  "applicants": 25,
  "salary": null,
  "salary_min": null,
  "salary_max": null,
  "salary_currency": null,
  "salary_period": null,
  "salary_annual_min": null,
  "salary_annual_max": null,
  "salary_source": null,
  "experience_years_min": 7,
  "visa_sponsorship": "unknown",
  "description_text": "Great people. Great places. Great projects. …",
  "recruiter": null
}
```

The run's `OUTPUT` record shows the **funnel** (how many jobs each filter step kept or dropped), the charged events, why the run stopped, and the available time ranges. The `HIRING_LEADS` record groups the delivered jobs by employer (see the agencies use case).

### Schedules and integrations

- **Schedules:** save your input as a task, enable `onlyNewSinceLastRun`, and schedule it hourly or daily for a job-alert feed with no repeats.
- **API:** `POST https://api.apify.com/v2/acts/sharplinelabs~linkedin-jobs-search/run-sync-get-dataset-items` returns the jobs in one call (for small runs); for bigger ones start a run and read the dataset. Clients: `apify-client` for Python and JavaScript.
- **Make, n8n, Zapier:** use the Apify app/node: *Run Actor* → *Get dataset items* → Google Sheets, Airtable, Slack, email.
- **Webhooks:** fire on `ACTOR.RUN.SUCCEEDED` to push new jobs to your app.
- **RSS:** the dataset's `?format=rss` export works in any feed reader.
- **AI agents (MCP):** connect `https://mcp.apify.com?tools=sharplinelabs/linkedin-jobs-search` in Claude Desktop, Claude Code, Cursor, VS Code or ChatGPT. Runs take a few minutes, so agents should start the run, poll it, then read the dataset (or the `JOBS_AI` record).

### FAQ

#### Do I need a LinkedIn account or cookies?

No. Everything comes from LinkedIn's public, logged-out job pages. We never log in and you never share credentials.

#### How fresh are the jobs?

The index is refreshed every hour, and the busiest areas every 30 minutes. `first_seen_at` tells you when we first saw each job.

#### Does every job get an employer apply link?

Every job gets an `apply_url`. Easy Apply jobs (~20%) link to LinkedIn. For off-site jobs we find the employer's own page for about 27% overall (36% in the US) as of 2026-10-26; in Complete mode the partner lookup fills most of the rest, and anything left points to the LinkedIn job page (`apply_source: "linkedin"`).

#### What's the difference between verified and probable links?

`board_verified` matched on a requisition ID, or an identical title in the same city with a single candidate. `board_title` is a close title match we couldn't confirm, so check it before applying. `applyLinks: "verified"` keeps only verified links (and Easy Apply).

#### Why use `experienceYears` instead of LinkedIn's seniority?

LinkedIn's own seniority label is often missing or vague: in our October 2026 sample, 55% of jobs were labelled "Not Applicable", yet 73% of those state the years required in the description, and "Mid-Senior level" covered anything from 2 to 8 years. `experienceYears` reads the requirement from the description itself ("3+ years", "minimum of 5 years", "mindestens 3 Jahre").

#### Can I get only remote jobs?

Yes: `workplace: ["remote"]`. The label comes from the job's own text. Jobs with no signal are `unknown`; add `"unknown"` to the list if you'd rather not miss them.

#### Can I get only jobs on Workday, Greenhouse or Lever?

Yes: `ats: ["workday", "greenhouse", "lever"]`. This keeps jobs whose employer link we found on those systems.

#### Why did my search return fewer jobs than expected?

Your rules matched fewer jobs in that time range, or the run hit `maxJobsToCheck` or your maximum cost. The `OUTPUT` funnel shows exactly which step dropped them. Try fewer exclusions, turn off `excludeReposts`, or use a longer time range.

#### Will I pay twice on a schedule?

No. With `onlyNewSinceLastRun`, jobs this search already delivered, or already checked and dropped, are skipped and not charged again.

#### Is there a LinkedIn jobs API?

LinkedIn's official job APIs are for partners only. This actor gives you an API over public job postings: one HTTP call returns JSON.

#### Can I use it from Python?

Yes: `pip install apify-client`, then `client.actor("sharplinelabs/linkedin-jobs-search").call(run_input={...})` and iterate `client.dataset(run["defaultDatasetId"]).iterate_items()`.

#### Can I export LinkedIn jobs to Excel or Google Sheets?

Yes. Export the dataset as Excel or CSV, or use the Make / n8n / Zapier integrations to append rows to Google Sheets.

#### I'm switching from another LinkedIn jobs actor. Do the inputs carry over?

Mostly. We use the common `timeRange`, `titleSearch`, `titleExclusionSearch`, `locationSearch` and `descriptionSearch` field names. Check the cheat-sheet for rule syntax.

#### What does Complete mode do with my account?

It starts one run of `bebity/linkedin-jobs-scraper` with your own Apify token, only for jobs still missing a confirmed link, up to `partnerMaxJobs`. You see it in your Console and pay that actor's price ($1 to $1.50 per 1,000 jobs, depending on your plan), plus our $0.20 per 1,000 filled links. It may not work on Apify's Creator plan, which can't run other developers' actors; then the run still finishes with our own links. Pick **Basic** to never start it.

#### What counts as a repost?

A job whose LinkedIn job ID already existed 2 or more days before it showed up as new. See "Reposts and recruitment agencies" above.

#### How do I find companies that can't fill a role?

Set `hardToFill: "only"` and `excludeAgencies: true`. You get roles LinkedIn shows as new but that were first created 30+ days ago (`hardToFillDays` changes the threshold), and the `HIRING_LEADS` record lists the employers behind them with company size and website.

#### Something's wrong. Where do I get help?

Open an issue on the **Issues** tab with your run ID. We read every one.

### Changelog

- **2026-10-28:** hard-to-fill roles for agencies: `job_age_days`, `id_created_estimate` and `hard_to_fill` on every job, `hardToFill` / `hardToFillDays` filters, the `HIRING_LEADS` per-employer record and a `hard_to_fill` dataset view. Job pages are now always read in English, so the experience-level, employment-type and industry filters no longer drop jobs whose LinkedIn page came back in another language. More employer apply links (new careers-site readers: Taleo, SuccessFactors fixes, Radancy, Avature, PeopleSoft, softgarden, gupy and more).

- **2026-10-27:** enriched fields (work arrangement with remote-only vs remote-OK, office days, remote scope, employment type, 33 job categories, skills, education, certifications, benefits, hours, language, extractive summary/responsibilities/requirements, parsed location, company size/industry/website/HQ from the company page) with `workArrangement`, `categories`, `languages` and `companySize` filters; `compatibility: "fantastic"` field names; phrases with `*` keep word order; AI exports keep the full description.

- **2026-10-27:** parsed salary (`salary_min` / `salary_max` / `salary_currency` / `salary_period`, yearly figures, `salary_source`) with a `minSalary` filter; `experience_years_min` with an `experienceYears` filter; `visa_sponsorship` (yes / no / unknown) with a `visaSponsorship` filter; `industries` / `industryExclusion` filters.

- **2026-10-27:** simpler input (core search on top, the rest under More filters); plain words now match the exact phrase (`data engineer` no longer matches "Data Center Engineer"); repost detection by job ID age (the old posted-date rule missed nearly all reposts); `excludeAgencies` with `is_agency` / `agency_reason`; `applyLinks` modes with the partner lookup on by default; `apply_url` is never empty (LinkedIn page fallback, `apply_source: "linkedin"`).

- **2026-10-27:** AI-friendly exports (`aiFormat`: YAML / Markdown / TOON, `descriptionMaxChars`, `ai` dataset view); optional partner apply-link lookup (`partnerApplyLinks`, beta); stricter regex guard; default `limit` is now 20 and `countries` defaults to the US (`[]` = all countries).

- **2026-10-27:** the built-in employer-board registry is used in every run (more apply links); `maxJobsToCheck` holds across a platform migration; accent-insensitive regex; `C++`, `C#` and `.NET` kept in quoted phrases.

- **2026-10-27:** first release: hourly index since 2026-10-24, boolean text filters, workplace classifier, employer apply links, `onlyNewSinceLastRun`, pay-per-event pricing.

# Actor input Schema

## `timeRange` (type: `string`):

Jobs that appeared on LinkedIn in this window. Our index starts on 2026-10-24, so longer ranges open on the dates shown (picking one early ends the run with a message and no charge).

## `countries` (type: `array`):

Default: United States. Add more countries, or clear the list to search every indexed country. The United Kingdom is GB.

## `titleSearch` (type: `array`):

One title per line; a job matching any line is kept. <code>data engineer</code> matches that phrase: Senior Data Engineer yes, Data Center Engineer no. Advanced (any line): <code>python AND django</code> both words anywhere · <code>dev\*</code> starts with · <code>^Senior</code> title starts with · <code>=Data Engineer</code> whole title · <code>NOT intern</code> · <code>(a OR b)</code> · <code>/regex/</code>. AND / OR / NOT in capitals; case and accents ignored.

## `titleExclusionSearch` (type: `array`):

Drop jobs whose title matches any line, e.g. <code>intern</code>, <code>senior</code>, <code>sales</code>. Same rules as Job titles.

## `locationSearch` (type: `array`):

Keep jobs whose location matches any line, e.g. <code>New York</code>, <code>California</code>, <code>Remote</code>. Same rules as Job titles. Leave empty for the whole country.

## `workplace` (type: `array`):

Remote, hybrid or on-site, read from the location, LinkedIn's tag and the description. Many posts don't say: those are Unknown.

## `seniority` (type: `array`):

LinkedIn's seniority level for the job.

## `employmentType` (type: `array`):

LinkedIn's employment type.

## `excludeAgencies` (type: `boolean`):

Drop jobs posted by staffing firms, recruiters, job boards and gig platforms: our list of large agencies, company names (Staffing, Recruiting, Talent...), LinkedIn's Staffing industry, and third-party wording such as "our client", C2C or W2. Each job carries is_agency and agency_reason either way.

## `excludeReposts` (type: `boolean`):

LinkedIn shows a re-listed job as just posted. We spot reposts by the LinkedIn job ID, which grows over time: a job whose ID already existed 2+ days before it appeared is a repost (often half of the 'new' jobs). Turn off to include them; each job carries is_repost.

## `applyLinks` (type: `string`):

Every job gets an apply_url. We first look for the employer's own job page on its careers site. In Complete mode, jobs we could not match are looked up with the partner actor bebity/linkedin-jobs-scraper, which runs on YOUR Apify account at its own price ($1.00-1.50 per 1,000 jobs; free-plan credit works) plus $0.0002 per link it fills (apply_source = partner). If the partner is unavailable on your plan or fails, the run still finishes. A job with no employer link falls back to its LinkedIn page (apply_source = linkedin).

## `limit` (type: `integer`):

Stop after this many jobs (newest first).

## `hardToFill` (type: `string`):

A hard-to-fill role is a job LinkedIn shows as new while its job ID is 30+ days old: the employer re-listed the same opening because it is still unfilled. Useful for recruitment and staffing agencies looking for clients. 'Only' keeps reposts automatically. Every job carries job_age_days and hard_to_fill; the HIRING_LEADS record groups the run's jobs by employer.

## `companySearch` (type: `array`):

Keep only these companies (one per line; same rules as Job titles; <code>=Meta</code> = exactly that name).

## `companyExclusionSearch` (type: `array`):

Drop these companies (one per line).

## `locationExclusionSearch` (type: `array`):

Drop jobs whose location matches any line.

## `descriptionSearch` (type: `array`):

Keep jobs whose description matches any line, e.g. <code>"visa sponsorship"</code> or <code>python AND aws</code>. Same rules as Job titles.

## `descriptionExclusionSearch` (type: `array`):

Drop jobs whose description matches any line, e.g. <code>"security clearance"</code>.

## `minSalary` (type: `integer`):

Keep jobs whose top of the pay range, converted to a year (hour x 2080, day x 260, week x 52, month x 12), is at least this, in the currency below. Pay comes from LinkedIn's salary box or a pay range in the description. When set, jobs with no stated pay or pay in another currency are dropped.

## `salaryCurrency` (type: `string`):

Currency of the minimum salary. Used only with Minimum salary; amounts are not converted between currencies.

## `experienceYears` (type: `array`):

The minimum years the description asks for ("3+ years", "at least 5 years", "5-7 years of experience"). Add Not stated to keep jobs that don't say.

## `visaSponsorship` (type: `string`):

Read from the description: "visa sponsorship available" is yes; "unable to sponsor", "without sponsorship" or "U.S. citizenship required" is no. Most posts say neither.

## `industries` (type: `array`):

Keep jobs whose LinkedIn industry matches any line (same rules as Job titles), e.g. <code>"software development"</code>. Jobs without an industry are dropped.

## `industryExclusion` (type: `array`):

Drop jobs whose LinkedIn industry matches any line, e.g. <code>"staffing and recruiting"</code>.

## `workArrangement` (type: `array`):

Finer than Workplace: separates fully remote jobs from remote-with-office ones. Read from LinkedIn's label and the description.

## `categories` (type: `array`):

Keep jobs in these categories (same 33 categories as Fantastic.jobs), mainly from the job title.

## `categoryExclusion` (type: `array`):

Drop jobs in any of these categories.

## `languages` (type: `array`):

Keep jobs whose description is written in these languages (detected from the text).

## `companySize` (type: `array`):

Keep jobs from companies of these sizes (LinkedIn's company page). Jobs whose company page can't be read are kept.

## `hasSalary` (type: `boolean`):

Keep jobs with a salary block on LinkedIn or a currency amount in the description.

## `maxApplicants` (type: `integer`):

Keep jobs with at most this many applicants (LinkedIn's count).

## `easyApply` (type: `string`):

LinkedIn Easy Apply jobs: keep all, only them, or none of them.

## `ats` (type: `array`):

Keep only jobs whose employer apply link is on these systems.

## `hardToFillDays` (type: `integer`):

How old a re-listed job's ID must be to count as hard to fill. Ages come from the job ID (exact to the day for recent IDs, a few weeks for IDs from before Oct 2026).

## `includeDetails` (type: `boolean`):

Open each job page: description, seniority, employment type, industries, applicants, salary, recruiter. Off = index fields only (cheaper event).

## `descriptionMaxChars` (type: `integer`):

0 (default) = the full description (description_text + description_html). Only set a number if you must save tokens: each job then gets description_snippet, the first N characters, instead of the full text. The description is what an AI needs most to judge a job, so we don't recommend cutting it.

## `aiFormat` (type: `string`):

Also save all delivered jobs as ONE compact record (key JOBS_AI in the run's key-value store, linked from OUTPUT.ai_export_url) for LLM prompts: title, company, location, date, workplace, seniority, type, salary, links and the full description (cut only if you set descriptionMaxChars). TOON and CSV-style rows use about 12-15% fewer tokens than JSON for the same content. No extra charge.

## `includeCompanyDetails` (type: `boolean`):

Read each company's public LinkedIn page once per run: size, employees, industry, website, headquarters, type, founded, followers. No extra charge; turn off for faster runs.

## `compatibility` (type: `string`):

For scripts written against fantastic-jobs/advanced-linkedin-job-search-api: adds copies of our fields under their column names.

## `onlyNewSinceLastRun` (type: `boolean`):

Skip jobs this same search already delivered (tracked in your key-value store 'lij-search-state'). Made for schedules.

## `maxJobsToCheck` (type: `integer`):

Most job pages to open in one run. Also caps the candidates kept after the text filters at 5x this number (newest first).

## `partnerMaxJobs` (type: `integer`):

Most jobs sent to the partner lookup in one run (Apply links = Complete), newest first. Also caps what the partner run may charge you: jobs x $0.0015 x 1.25 + $0.001.

## Actor input object example

```json
{
  "timeRange": "24h",
  "countries": [
    "US"
  ],
  "titleSearch": [
    "data engineer",
    "machine learning"
  ],
  "titleExclusionSearch": [
    "intern",
    "principal"
  ],
  "locationSearch": [
    "New York",
    "Remote",
    "*Area"
  ],
  "excludeAgencies": false,
  "excludeReposts": true,
  "applyLinks": "complete",
  "limit": 20,
  "hardToFill": "any",
  "companySearch": [
    "Google",
    "=Meta",
    "^Amazon"
  ],
  "descriptionSearch": [
    "python AND (django OR flask)",
    "\"visa sponsorship\""
  ],
  "salaryCurrency": "USD",
  "visaSponsorship": "any",
  "industries": [
    "\"software development\"",
    "\"financial services\""
  ],
  "hasSalary": false,
  "easyApply": "any",
  "hardToFillDays": 30,
  "includeDetails": true,
  "descriptionMaxChars": 0,
  "aiFormat": "none",
  "includeCompanyDetails": true,
  "compatibility": "none",
  "onlyNewSinceLastRun": false,
  "maxJobsToCheck": 2000,
  "partnerMaxJobs": 100
}
```

# Actor output Schema

## `results` (type: `string`):

One item per delivered job (title, company, location, apply link and its source, workplace, details). Opens the overview view; switch to the ai or apply views in Console.

## `aiResults` (type: `string`):

The same jobs with the compact AI field set (title, company, location, posted date, workplace, seniority, employment type, salary, apply link, LinkedIn URL, full description).

## `runSummary` (type: `string`):

JSON record: filter funnel (jobs kept or dropped per step), charged events, why the run stopped, available time ranges, partner lookup status and ai_export_url.

## `aiExport` (type: `string`):

Only when aiFormat is yaml, markdown or toon: all delivered jobs in that token-lean format, empty fields dropped, full descriptions (cut only when descriptionMaxChars is set). Not written when aiFormat is none.

## `hiringLeads` (type: `string`):

JSON list, one row per employer in this run (recruitment agencies left out): open roles, hard-to-fill roles (re-listed, open 30+ days), oldest role age, titles, locations, company size, industry and website, example jobs. Sorted by hard-to-fill roles.

# 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 = {
    "timeRange": "24h",
    "countries": [
        "US"
    ],
    "titleSearch": [
        "data engineer",
        "machine learning"
    ],
    "limit": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("sharplinelabs/linkedin-jobs-search").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 = {
    "timeRange": "24h",
    "countries": ["US"],
    "titleSearch": [
        "data engineer",
        "machine learning",
    ],
    "limit": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("sharplinelabs/linkedin-jobs-search").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 '{
  "timeRange": "24h",
  "countries": [
    "US"
  ],
  "titleSearch": [
    "data engineer",
    "machine learning"
  ],
  "limit": 20
}' |
apify call sharplinelabs/linkedin-jobs-search --silent --output-dataset

```

## MCP server setup

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

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/MNlxuVsYEadl2jFHT/builds/J5VBjOhJHGeKVL1ES/openapi.json
