# LinkedIn Jobs Scraper with Company Contacts (`garnet_puppet/linkedin-jobs-contact-scraper`) Actor

Scrape LinkedIn job postings without a login or cookies, then follow each hiring company to its own website and pull contact e-mail addresses, checked against live MX records. Also returns the person who posted the job when LinkedIn shows one.

- **URL**: https://apify.com/garnet\_puppet/linkedin-jobs-contact-scraper.md
- **Developed by:** [Mehmet](https://apify.com/garnet_puppet) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job 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?

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 Jobs Scraper with Company Contacts

Scrapes LinkedIn job postings **and goes to the hiring company's own website to find
verified contact details.**

You give it a job title and a location. You get back the postings, and for each one the
company's real website plus e-mail addresses pulled from that website and checked against
live MX records.

No LinkedIn account, no cookies, no session token. It reads the same public pages
LinkedIn serves to a logged-out visitor.

***

### How this compares — checked, not claimed

I went through eight other LinkedIn job scrapers on this Store and read their full input
and output schemas on 7 September 2026. I could not run them: my Apify plan does not permit
running other people's public Actors, and the platform says so outright. So this is a
comparison of what they *say* they return, not of results I benchmarked.

**Two of the eight do deliver an e-mail address.** `worldunboxer` takes the first e-mail in
the job description; `fantastic-jobs` extracts a hiring-manager e-mail with AI. Both read
only the posting text. I measured that source: **15% of postings contain an e-mail at all.**

**Four of the eight resolve the company's website.** They hand you the URL.

What I have not found in any of them, and what this Actor does: **take that website, crawl
its contact pages, and return e-mail addresses checked against live MX records** — the 92%
step chained onto an 82% step, instead of stopping at the URL.

Where they were ahead: five of the eight return the person who posted the job. This Actor
did not, until this check found the gap. It does now.

### The numbers, measured

I ran this against live LinkedIn postings on 7 September 2026 before publishing. Sample:
40 postings for the job-data numbers, 24 postings for the company-website chain.

| | Rate |
|---|---|
| Postings returning title, company and full description | **100%** |
| Postings that expose the company's LinkedIn page | **100%** |
| **Company's own website resolved from that page** | **92%** |
| E-mail address inside the job description itself | 15% |
| Website in the job description itself | 10% |
| Postings that name the person who posted them | 20% |

That 92% is the number that matters, and it is why this Actor exists. The job description
almost never contains a contact — only 15% do. The company's website almost always can be
found, and a business website usually publishes an address.

For what happens after the website is resolved, my
[Website Contact Scraper](https://apify.com/garnet_puppet/website-contact-scraper)
measured **82% of responding business websites** giving up an e-mail or phone number, on a
sample of 100 real businesses. Chaining the two: expect a contact on roughly **three out of
four** postings. I have not yet measured the full chain end to end on a large sample, and I
will publish that number here when I have — I would rather show you the two measured links
than a single number I guessed.

### What you get per job

```json
{
  "jobId": "4400000001",
  "url": "https://www.linkedin.com/jobs/view/4400000001/",
  "title": "Product Marketing Manager",
  "company": "Rentman",
  "companyLinkedInUrl": "https://nl.linkedin.com/company/rentman",
  "companyWebsite": "https://rentman.io",
  "location": "Utrecht, Netherlands",
  "postedText": "9 hours ago",
  "applicants": 29,
  "seniority": "Mid-Senior level",
  "employmentType": "Full-time",
  "jobFunction": "Marketing",
  "industries": "Software Development",
  "description": "…full posting text…",
  "posterName": "Barbara Horváth",
  "posterHeadline": "Recruitment Specialist at Tetra Pak",
  "posterProfileUrl": "https://hu.linkedin.com/in/barbara-horvath-4361a089",
  "contacts": [
    {
      "email": "work@rentman.io",
      "source": "job-description",
      "emailStatus": "deliverable_domain"
    },
    {
      "email": "info@rentman.io",
      "source": "company-website",
      "foundOn": "https://rentman.io/contact",
      "emailStatus": "deliverable_domain"
    }
  ],
  "scrapedAt": "2026-09-07T21:14:00.000Z"
}
```

`posterName` / `posterHeadline` / `posterProfileUrl` are the person LinkedIn shows as having
posted the job. Present on about one posting in five — when it is there, it is usually the
recruiter or the hiring manager, which is often a better lead than a generic company inbox.

Every contact says where it came from: `job-description` (in the posting text) or
`company-website` (found by scanning the company's site, with the exact page in `foundOn`).
You can filter on that.

**`contacts` absent** means contact lookup was off. **`contacts: []`** means it was on and
nothing was found. Those are different things and the output keeps them different.

### About `emailStatus`

Apify's platform blocks outbound port 25, so no scraper here can do a real SMTP mailbox
check — including the ones that imply they can. What this Actor does is a DNS MX lookup:
`deliverable_domain` means the domain accepts mail, not that the individual mailbox exists.
It is stated plainly rather than dressed up.

Verification is included in the base price. It is not sold as an add-on.

### Filters — and something you should know about them

LinkedIn's public search endpoint accepts a pile of filter parameters. **Most of them do
nothing.** I checked each one by reading the postings that came back, on 7 September 2026:

| Filter | LinkedIn applies it? | Evidence |
|---|---|---|
| Posted within N days | **yes** | unfiltered: 0 of 6 posted in the last day; filtered: 6 of 6 |
| Company | **yes** | asked for company 1035, got 6 of 6 Microsoft |
| Employment type | **no** | asked for Contract, got six Full-time postings |
| Seniority | **no** | identical results to no filter |
| Remote / hybrid | **no** | 0 of 6 remote either way |
| Under 10 applicants | **no** | returned postings with 74–200 applicants |

So this Actor sends LinkedIn only the two that work, and applies the rest itself, on the
real fields read from each posting. That means they actually work — and it means a filtered
posting is downloaded before it is discarded. **Discarded postings are never charged.**

`Max jobs` counts postings *delivered*, not postings looked at. With a strict filter the
run keeps paging until it has what you asked for, up to ten times that many postings
examined; then it stops and tells you the filter was too strict rather than quietly
returning less.

Available filters: **posted within N days** and **company id** (LinkedIn-side);
**employment type**, **seniority**, **remote/hybrid/on-site**, **max applicants**,
**title must contain**, **title must not contain** (applied here).

### Settings

Three fields on the first screen: **keywords**, **location**, **max jobs**. Everything else
is folded away and optional.

- **Find company contact details** — off by default. Turning it on adds requests (the
  company page plus a few pages of the company website), so you opt in.
- **Only return jobs that have a contact** — skips postings where nothing was found. You
  are not charged for skipped postings.
- **Advanced** — paste LinkedIn search URLs straight from your browser, or use LinkedIn's
  own `geoId` for precise locations.

### What it does not do

- **No profiles, no connections, no messages.** Job postings and public company pages only.
- **No login, no cookies.** If a page needs an account, this Actor does not go there.
- **Expired postings are skipped, not billed.** LinkedIn returns HTTP 200 with an empty
  shell for dead postings; those produce no record and no charge.
- **It is not a browser.** Plain HTTP, which is why it is cheap. If LinkedIn ever moves this
  data behind a browser check, this Actor will report fewer results rather than quietly
  switching to something more expensive.

### Cost

Pay per event. You are charged for a job only when a record is actually delivered, and a
second, smaller event when contact details were found for it. If pricing is ever
misconfigured on my side, the run stops instead of handing you data for free — a failed run
is easier to notice than a silently unbilled one.

### Rate limiting

Requests are spaced deliberately. This Actor is not built to hammer LinkedIn, and running
it with a huge `maxJobs` will be slow on purpose.

***

*Built by [garnet\_puppet](https://apify.com/garnet_puppet). If a field is missing or a
company website resolves wrong, tell me which posting and I will fix it — the parsers are
tested against real page structures and I would rather know.*

# Actor input Schema

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

What to search for, e.g. "marketing manager" or "python developer".

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

City, region or country, e.g. "Amsterdam", "Germany", "United States".

## `maxJobs` (type: `integer`):

How many job postings to return. Each delivered posting is one charged result.

## `findCompanyContacts` (type: `boolean`):

Look up the company's own website through its LinkedIn page, then scan that site for e-mail addresses. Costs extra requests, so it is off by default.

## `verifyEmails` (type: `boolean`):

Look up MX records for every address found. Included in the base price.

## `onlyWithContact` (type: `boolean`):

Skip postings where no e-mail or phone number could be found. Skipped postings are not charged.

## `maxPagesPerCompany` (type: `integer`):

How many contact-intent pages to read on each company site (/, /contact, /about, /impressum, ...).

## `postedInDays` (type: `integer`):

Applied by LinkedIn. 0 means no date filter.

## `companyId` (type: `string`):

Applied by LinkedIn. Returns only this company's postings. Find it in the company page URL or its LinkedIn jobs link.

## `employmentType` (type: `string`):

Applied after reading each posting.

## `seniority` (type: `string`):

Applied after reading each posting.

## `workplace` (type: `string`):

Applied after reading each posting, by looking at the job title and location.

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

Only keep postings with at most this many applicants -- less competition. 0 means no limit. Postings where LinkedIn does not show a count are kept.

## `titleInclude` (type: `array`):

Keep only postings whose title contains at least one of these words.

## `titleExclude` (type: `array`):

Drop postings whose title contains any of these words, e.g. "senior", "intern".

## `searchUrls` (type: `array`):

Paste LinkedIn job-search URLs straight from your browser. When set, the keyword and location fields above are ignored.

## `geoId` (type: `string`):

LinkedIn's own location id. More precise than the location text; overrides it when set.

## `includeDescription` (type: `boolean`):

The complete posting text. Turn off for smaller results.

## Actor input object example

```json
{
  "keywords": "marketing manager",
  "location": "Netherlands",
  "maxJobs": 50,
  "findCompanyContacts": false,
  "verifyEmails": true,
  "onlyWithContact": false,
  "maxPagesPerCompany": 4,
  "postedInDays": 0,
  "companyId": "",
  "employmentType": "",
  "seniority": "",
  "workplace": "",
  "maxApplicants": 0,
  "titleInclude": [],
  "titleExclude": [],
  "searchUrls": [],
  "geoId": "",
  "includeDescription": true
}
```

# Actor output Schema

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

One record per job posting: title, company, location, seniority, employment type and the full description -- plus, when company contact lookup is on, the company's own website and verified contact details.

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

Counts for the whole run: jobs delivered, expired postings skipped, company websites resolved, and how many jobs ended up with a contact.

# 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 = {
    "keywords": "marketing manager",
    "location": "Netherlands"
};

// Run the Actor and wait for it to finish
const run = await client.actor("garnet_puppet/linkedin-jobs-contact-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 = {
    "keywords": "marketing manager",
    "location": "Netherlands",
}

# Run the Actor and wait for it to finish
run = client.actor("garnet_puppet/linkedin-jobs-contact-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 '{
  "keywords": "marketing manager",
  "location": "Netherlands"
}' |
apify call garnet_puppet/linkedin-jobs-contact-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,garnet_puppet/linkedin-jobs-contact-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/Svwo5rmhtiJKvv0Aa/builds/9FBd2bZl14WrenX1d/openapi.json
