# LinkedIn Hiring Leads — Company Contacts & Emails (`qualifyops/hiring-leads-finder-company-contacts`) Actor

Hiring leads finder: find companies that are hiring and their company contact details from public sources. Build hiring-intent B2B lead lists for sales and recruiting outreach.

- **URL**: https://apify.com/qualifyops/hiring-leads-finder-company-contacts.md
- **Developed by:** [QualifyOps](https://apify.com/qualifyops) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 results

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/platform/actors/running/actors-in-store#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

### Try it in 60 seconds

**You give:** a public LinkedIn job-search URL (a role + location).
**You get:** one scored record per **company that is hiring** — roles, a deterministic hiring signal, the likely commercial reason, public emails/phones, contact page, and the evidence job URLs.
**Price:** **$0.01 per company** (intro). A 100-company run costs about **$1**.
**Run it now:** press **Start** above and paste a job-search URL — results stream to the dataset (export JSON/CSV/Excel).

**Real sample — live run, 8 companies, total cost $0.0099:**

- Albury Wodonga Health — signal `general_hiring`, `awh.foundation@awh.org.au`, +61 2 6051 7111
- Cairns Family & Cosmetic Dental — +61 7 4036 4391
- Ramsay Health Care — +61 2 9433 3444
  Deduped to one row per company, junk-filtered (no copyright-date phones, no widget emails).

**For agents & automation:** call it via the Apify API/MCP — one run returns deduped, CRM-ready company records with contacts + signal. Schedule it for a recurring hiring-signal feed.

***

## Hiring Leads Finder — Company Contacts & Growth Signals

Turn fresh **LinkedIn job-search results** into **one deduplicated record per company**:
which roles they're hiring, what the hiring most likely means commercially,
public contact routes, and a transparent opportunity score — with evidence.

The dominant LinkedIn Jobs scraper already gives you *raw job rows*. This Actor
completes the chain those rows leave to you:

> raw job rows → **unique companies actively hiring → what the hiring implies → public contact routes → evidence → transparent score**

***

### Why this instead of a raw job scraper

A LinkedIn/Indeed job scraper returns one row **per posting** — 30 postings from the
same company, no contacts, and no way to tell what the hiring *means*. You still have to
collapse rows to companies, find contacts, and decide who to reach first.

| | Raw job scraper | Generic "hiring lead" actors | **Hiring Leads Finder** |
|---|---|---|---|
| Output unit | one row per **job** | one row per lead | one row per **company** (deduplicated) |
| Hiring signal | none | opaque | **deterministic, auditable classes** |
| Score | none | black-box | **transparent — every point explained** |
| Public contacts | none | sometimes | **emails + phones + contact page, one pass** |
| Evidence | — | — | **source job URLs per company** |

Pair it with any LinkedIn jobs scraper (it calls a proven one for you) and it returns the
part you actually wanted: **which companies are hiring, what it implies, and how to reach them.**

***

### What it does

1. Calls the proven upstream **LinkedIn Jobs Scraper** (`curious_coder/linkedin-jobs-scraper`) to fetch fresh public job rows for your search URL(s).
2. Normalizes company identity (website domain → LinkedIn URL → cleaned name).
3. Groups job rows into **one record per company** and collapses duplicate rows.
4. Determines the latest posting date and the number of matching roles.
5. Applies optional **role-keyword** and **freshness** filters.
6. Classifies the **hiring signal** with deterministic, auditable rules.
7. Scans the public company website for **emails, phones, a contact page and social links**.
8. Calculates a **transparent opportunity score** (every point is explained).
9. Returns one dataset row per company, sorted by score.

No CRM, no historical monitoring, no AI-written outreach, no opaque score.

***

### Input

| Field | Type | Notes |
|-------|------|-------|
| `jobSearchUrls` *(required)* | array | One or more public LinkedIn job-search URLs. |
| `maxJobs` | integer | Upper bound on raw rows from upstream. |
| `roleKeywords` | array | Keep only titles containing one of these (case-insensitive). |
| `freshnessDays` | integer | Keep only jobs posted within N days. Unparseable dates are kept and flagged. |
| `includeWebsiteContacts` | boolean | Default `true`. Scan company websites for public contacts. |
| `maxCompanies` | integer | Return only the top N by score. |
| `requestTimeoutSecs`, `maxPagesPerSite`, `maxConcurrency` | integer | Website-scan tuning. |

See [`sample_input.json`](./sample_input.json).

***

### Output (one row per company)

`company_name`, `company_website`, `company_linkedin`, `company_domain`,
`new_jobs_count`, `roles_hiring`, `latest_job_date`, `hiring_signal`,
`commercial_reason`, `classification_reasons`, `matched_titles`,
`public_emails`, `public_phones`, `contact_page`, `social_links`,
`signal_score`, `score_reasons`, `evidence_job_urls`, `upstream_actor`,
`upstream_run_id`, `status`, `error`, `processed_at`.

See [`sample_output.json`](./sample_output.json).

#### Hiring-signal classes

`sales_expansion`, `marketing_expansion`, `technical_product_investment`,
`operations_expansion`, `new_location_or_service_capacity`,
`clinical_practitioner_expansion`, `general_hiring`.

The dominant class is the one matching the most distinct job titles; ties break
by a fixed priority order. Every record carries the matched titles and the
reasons, so the classification is auditable.

***

### Opportunity score (transparent weights)

The score measures **actionability**, not generic company quality. It is the
sum of the fired components below, capped at **100**:

| Component | Weight | Fires when |
|-----------|-------:|------------|
| Multiple related vacancies | +15 | 2 or more current roles |
| High vacancy volume | +10 | 4 or more current roles |
| Senior / expansion-linked role | +15 | a senior/leadership title is present |
| Recent posting | +15 | latest matching role within the freshness window (default 7 days) |
| Company website resolved | +10 | a website was found |
| Public email found | +20 | at least one public email |
| Public phone found | +10 | at least one public phone |
| Dedicated contact page | +10 | a contact page was found |
| Clear signal classification | +10 | signal is not `general_hiring` |
| Multiple evidence URLs | +5 | 2 or more source job URLs |

Each fired component appears verbatim in `score_reasons` (e.g. `+20 Public email found`).

***

### Example uses

**General B2B.** Australian companies hiring several sales, marketing,
operations or technical roles in the last seven days → contactable company
opportunities.

**Aesthetic clinics.** Filter `roleKeywords` for `cosmetic nurse`,
`dermal therapist`, `laser therapist`, `clinic manager`, `women's health` to
surface clinics showing likely service-capacity or location expansion.

***

### Limitations & compliance boundary

- **Public data only.** Contacts come from publicly published company web pages
  and the upstream's public LinkedIn job data. No email verification, no
  private-data access, no gated profiles.
- **Hiring is a signal, not proven purchase intent.** The commercial reason is
  an interpretation, presented as a hypothesis to prioritise outreach — not a
  guarantee.
- Some public pages embed third-party widget emails (e.g. support vendors);
  these can appear among `public_emails`. Review before outreach.
- Posting dates from the source vary in format; unparseable dates are kept and
  flagged (`status: partial`) rather than guessed.
- Respect LinkedIn's and each website's terms; use results for legitimate B2B
  outreach consistent with applicable law (e.g. the Australian Spam Act —
  include identification and an unsubscribe path in any email).

***

### Pricing (recommendation — not validated)

> Working hypothesis: **$0.02 per qualified company opportunity**, or
> **$20 per 1,000 output companies**.

This is an unvalidated starting point, stated separately from technical
completion. It should be confirmed by real transactions, not by this document.

***

### Related actor

Working from a list of company **websites** instead of job searches? See
**Website Lead Qualifier & Contact Finder** (same author) — it turns any list of
company sites into clean, scored lead records with public emails, phones and socials:
https://apify.com/qualifyops/website-lead-qualifier-contact-finder

***

### Upstream dependency

See [`CONTRACT.md`](./CONTRACT.md). Requires `APIFY_TOKEN` at runtime so the
platform can call the upstream Actor. The upstream call uses `scrapeCompany: true`
to resolve company websites for contact extraction.

***

### Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install beautifulsoup4 httpx pytest
python -m pytest -q          # unit + fixture tests
python scripts/dry_run.py    # offline end-to-end on the bundled fixture

## Live end-to-end against the Apify platform (needs a token):
export APIFY_TOKEN=apify_api_xxx
pip install apify-client
python scripts/live_run.py --url "<linkedin job-search url>" --max-jobs 40 --freshness-days 14
```

# Actor input Schema

## `jobSearchUrls` (type: `array`):

One or more public LinkedIn job-search result URLs (e.g. a filtered search for a role and location).

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

Upper bound on raw job rows requested from the upstream scraper. Leave empty to use the upstream default.

## `roleKeywords` (type: `array`):

Only keep jobs whose title contains one of these keywords (case-insensitive). Leave empty to keep all roles.

## `freshnessDays` (type: `integer`):

Only keep jobs posted within this many days. Jobs with an unparseable date are kept and flagged. Leave empty to keep all.

## `includeWebsiteContacts` (type: `boolean`):

Scan each resolved company website for public emails, phones, a contact page and social links.

## `maxCompanies` (type: `integer`):

Return only the top N companies by opportunity score. Leave empty to return all.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each company web page before giving up.

## `maxPagesPerSite` (type: `integer`):

Homepage plus likely contact/about/team pages scanned per company website.

## `maxConcurrency` (type: `integer`):

Number of company websites scanned in parallel. Lower if sites are slow or fragile.

## Actor input object example

```json
{
  "jobSearchUrls": [
    {
      "url": "https://www.linkedin.com/jobs/search/?keywords=cosmetic%20nurse&location=Australia"
    }
  ],
  "roleKeywords": [],
  "includeWebsiteContacts": true,
  "requestTimeoutSecs": 20,
  "maxPagesPerSite": 4,
  "maxConcurrency": 5
}
```

# Actor output Schema

## `companies` (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 = {
    "jobSearchUrls": [
        {
            "url": "https://www.linkedin.com/jobs/search/?keywords=cosmetic%20nurse&location=Australia"
        }
    ],
    "roleKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("qualifyops/hiring-leads-finder-company-contacts").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 = {
    "jobSearchUrls": [{ "url": "https://www.linkedin.com/jobs/search/?keywords=cosmetic%20nurse&location=Australia" }],
    "roleKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("qualifyops/hiring-leads-finder-company-contacts").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "jobSearchUrls": [
    {
      "url": "https://www.linkedin.com/jobs/search/?keywords=cosmetic%20nurse&location=Australia"
    }
  ],
  "roleKeywords": []
}' |
apify call qualifyops/hiring-leads-finder-company-contacts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=qualifyops/hiring-leads-finder-company-contacts",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/JS8oF06OdDEixZUWj/builds/QrFTDPpd7bFjmGsOG/openapi.json
