# LinkedIn Ghost Job Detector & Checker - Apply Link Verifier (`kamerozkan/linkedin-job-apply-link-verifier`) Actor

Ghost job detector, checker & apply-link verifier for LinkedIn job rows. Recovers the exact official application link, flags ghost, expired & mismatched listings, blocks unverified rows before they reach your job board. No LinkedIn login, cookies or credentials. Confidence score per row.

- **URL**: https://apify.com/kamerozkan/linkedin-job-apply-link-verifier.md
- **Developed by:** [Kamer Ozkan](https://apify.com/kamerozkan) (community)
- **Categories:** Jobs
- **Stats:** 3 total users, 2 monthly users, 90.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 verified job decisions

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/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

Recover exact company application links and stop unsafe job rows before they
reach your users.

LinkedIn job feeds often return an empty external apply URL, a copy of the
LinkedIn job URL, or a generic company careers page. This Actor takes the job
rows you already have, reconciles each one against public employer and ATS
evidence, and returns a safe action for your workflow.

It is built for job boards, recruiting products, staffing automations, and job
data pipelines. It is not another job scraper.

### What it fixes

| Feed problem | Actor decision |
|---|---|
| External apply URL is missing | Finds and proves the exact employer or ATS route |
| Apply URL opens a generic careers board | Resolves the board to the exact job |
| LinkedIn row remains visible after the official job closes | Returns `EXPIRED` with no publishable URL |
| Company, title, location, or job content conflicts | Returns `SOURCE_MISMATCH` or `STATUS_CONFLICT` |
| The job only supports LinkedIn Easy Apply | Returns `LINKEDIN_EASY_APPLY` and the LinkedIn route |
| Evidence is not strong enough | Returns `AMBIGUOUS`, publishes no URL, and does not charge |

The safety rule is simple: **no proof, no link**.

### Try it in 60 seconds

1. Keep the two prefilled example jobs.
2. Click **Start**.
3. Open the **Verified apply routes** dataset view.
4. Use `actionUrl` only when `safeToPublish` is `true`.

The prefill demonstrates both supported outcomes: an exact company-owned
application page and a LinkedIn Easy Apply route. No LinkedIn account, cookies,
or browser profile are required.

### Input

Use either an existing Apify dataset or paste rows directly. Common LinkedIn
scraper field names are detected automatically.

Each row needs:

- LinkedIn job URL or job ID
- job title
- company
- location

An existing `applyUrl`, `companyApplyUrl`, or company website is optional. It
is treated as a hint and must pass the same verification policy.

```json
{
  "rows": [
    {
      "jobId": "4441060956",
      "jobUrl": "https://www.linkedin.com/jobs/view/4441060956",
      "jobTitle": "Account Executive - Travel",
      "companyName": "Nike Communications, Inc.",
      "location": "New York, NY"
    }
  ],
  "maxItems": 25
}
```

For an existing dataset, provide its ID instead:

```json
{
  "datasetId": "YOUR_DATASET_ID",
  "maxItems": 500
}
```

### Output

Every input row produces an auditable decision. A verified result looks like:

```json
{
  "status": "ACTIVE",
  "actionUrl": "https://nikecommunicationsinc.applytojob.com/apply/eDPh7BCIhx/Account-Executive-Travel",
  "safeToPublish": true,
  "reviewRequired": false,
  "usefulClassification": true,
  "confidence": 1,
  "sourceProvider": "jazzhr",
  "evidence": [
    "linkedin_apply_method:OFFSITE",
    "official_site_to_career_source_verified",
    "generic_board_resolved_to_exact_job",
    "conservative_official_inventory_match"
  ]
}
```

Important output fields:

| Field | Use |
|---|---|
| `actionUrl` | Route your product may open or publish |
| `safeToPublish` | Machine-readable release gate |
| `status` | Final decision for the job row |
| `reviewRequired` | Whether the row should enter a manual queue |
| `confidence` | Strength of the match |
| `sourceProvider` | Verified ATS or official source |
| `evidence` | Checks supporting the decision |
| `requestAccounting` | Requests used by each verification layer |
| `original` | Original input row, unchanged |

#### When the official job is gone

When the employer has removed a posting, the Actor says so instead of guessing. This
record comes from a real run; values are exactly as returned, only the long `evidence`
array is shortened here. It shows three guarantees at once:

- **No fabricated URLs.** `actionUrl` and `officialUrl` are `null` because no safe apply
  page exists. A missing value is always `null`, never a guessed link.
- **Deliberate lifecycle status.** `EXPIRED` is declared only after the complete official
  inventory was checked, as the `evidence` trail shows.
- **Publish protection built in.** `safeToPublish` flips to `false`, so a job board or
  alert email never ships a dead link.

```json
{
  "jobId": "4415520094",
  "title": "Staff/Sr. Software Engineer, Search Data Infrastructure - Slack",
  "company": "Slack",
  "location": "Seattle, WA",
  "linkedinUrl": "https://www.linkedin.com/jobs/view/4415520094",
  "status": "EXPIRED",
  "actionUrl": null,
  "officialUrl": null,
  "applyMethod": "OFFSITE",
  "safeToPublish": false,
  "reviewRequired": false,
  "confidence": 0.95,
  "sourceProvider": "workday",
  "evidence": [
    "verified_company_website_map",
    "ats_link_on_career_page",
    "workday_cxs_api:query=staff sr software engineer search data infrastructure slack",
    "official_inventory_empty",
    "official_sitemaps_scanned:14",
    "complete_employer_owned_inventory_checked",
    "complete_official_inventory_no_safe_match",
    "expired_classification_without_url"
  ],
  "checkedAt": "2026-07-28T14:34:29.121893+00:00",
  "schemaVersion": "1.0"
}
```

### Decision statuses

| Status | Meaning |
|---|---|
| `ACTIVE` | Exact live official job route verified |
| `AUTHORIZED_APPLY_ROUTE` | Valid source-owned application route verified |
| `LINKEDIN_EASY_APPLY` | No external route is expected; use the LinkedIn URL |
| `EXPIRED` | The exact official job is no longer available |
| `SOURCE_MISMATCH` | The row appears to belong to a different hiring organization |
| `STATUS_CONFLICT` | Trusted sources disagree about the current state |
| `AMBIGUOUS` | Evidence is insufficient; no URL is published and the row is not charged |

### Why this is different

Most LinkedIn job Actors collect listings. Official career-site APIs sell
their own job inventory. This Actor works between those products and your
publishing system:

1. It preserves your existing job row.
2. It checks customer-supplied links instead of trusting them.
3. It resolves generic ATS boards to the exact job.
4. It compares LinkedIn, employer, ATS, and live detail-page evidence.
5. It blocks expired, mismatched, conflicting, and unproven routes.

Search results and job mirrors may help discover a candidate, but they can
never become a publishable answer on their own.

### Quality evidence

The latest clean regression used 100 previously unseen LinkedIn job rows with
static resolution memory disabled:

- 98 useful classifications
- 97 safe action routes
- 2 rows safely held as `AMBIGUOUS`
- 0 unsafe guessed URLs
- 697 total HTTP requests
- 468.6 seconds on the local reference run

The production build also passed private Apify cloud tests:

- 2 normal rows: 2 useful, 2 publishable, 0 held
- 1 unresolved row: 0 publishable, correctly held for review
- 42 automated regression tests passed

These are measured tests, not a promise that every future source will resolve
automatically. The product promise is that an unproven route is never marked
safe.

### Pricing

The Actor charges **$2.00 per 1,000 verified job decisions**.

- $0.002 per useful decision
- `AMBIGUOUS` rows are delivered without charge
- failed runs do not create useful-decision charges
- Apify platform usage is included
- no subscription or per-seat fee

Example: verifying 25,000 useful job rows costs $50.

### Production workflow

For a recurring feed:

1. Run your existing LinkedIn or job scraper.
2. Pass its dataset ID to this Actor.
3. Publish rows where `safeToPublish` is `true`.
4. Send `reviewRequired` rows to a review queue.
5. Schedule the workflow or connect it through webhooks, Make, Zapier, n8n,
   or the Apify API.

The default batch is 25 jobs for easy inspection. A run accepts up to 500
jobs. Cold companies take longer because their public website, ATS, and job
evidence must be discovered live.

#### Wire it into Make, n8n, or your own webhook

Add a webhook for the `ACTOR.RUN.SUCCEEDED` event (Actor page > Integrations). Apify
posts this payload to your endpoint the moment a run finishes:

```json
{
  "eventType": "ACTOR.RUN.SUCCEEDED",
  "createdAt": "2026-07-28T15:02:44.906Z",
  "eventData": {
    "actorId": "TW4QDjkomVJH1SPsd",
    "actorRunId": "xYzAbCdEfGhIjKlM1"
  },
  "resource": {
    "id": "xYzAbCdEfGhIjKlM1",
    "status": "SUCCEEDED",
    "startedAt": "2026-07-28T15:00:48.634Z",
    "finishedAt": "2026-07-28T15:02:44.703Z",
    "defaultDatasetId": "qRsTuVwXyZaBcDeF2",
    "stats": { "durationMillis": 115406, "computeUnits": 0.0321 }
  }
}
```

Your workflow then fetches the verified rows in one call:

```
GET https://api.apify.com/v2/datasets/{resource.defaultDatasetId}/items?format=json&clean=1
```

In Make and n8n you can skip the webhook entirely: both have a native Apify integration
with a run-finished trigger and a get-dataset-items action, so a scheduled
verify-then-publish flow takes two modules.

This Actor is also exposed to AI agents through Apify's MCP server
(mcp.apify.com): an agent can discover it by search and run it with the same
pay-per-event billing, with no separate integration.

### Responsible use and limits

- The Actor checks public web evidence and does not require LinkedIn login
  credentials.
- Source websites can throttle, remove, or change public pages.
- An active official listing proves that an application route exists. It
  cannot prove a hiring manager's private intent or guarantee that a role will
  be filled.
- Review the terms and laws that apply to your data source and use case.

# Actor input Schema

## `datasetId` (type: `string`):

Read job rows directly from another Actor's dataset (any LinkedIn or job scraper). Common field names are detected automatically, no mapping needed. Leave empty when pasting rows below.

## `rows` (type: `array`):

Paste job rows as JSON. Each row needs a LinkedIn job URL or ID plus title, company, and location. An existing applyUrl or company website is treated as a hint and re-verified, never trusted blindly.

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

Hard cap on processed rows and therefore on cost: billing is $0.002 per useful decision, AMBIGUOUS rows are delivered free, failed runs charge nothing. Start with 25 to inspect the evidence trail, scale to 500 per run.

## `autoDiscoverCompanyWebsite` (type: `boolean`):

Recommended on. Discovers the employer's official website from public evidence when the row lacks one; without it, unknown companies cannot be resolved to exact ATS routes and are held as AMBIGUOUS (free) instead of guessed.

## `useReaderFallback` (type: `boolean`):

Recommended on. Uses a second public read path when the direct LinkedIn page is unavailable, recovering rows that would otherwise be held as AMBIGUOUS. No LinkedIn login or cookies are used either way.

## `maxSearchQueries` (type: `integer`):

Upper bound on public search queries per row. Higher values improve recall on cold companies at the cost of run time; the default suits recurring feeds where most employers repeat.

## `maxCandidates` (type: `integer`):

Upper bound on candidate pages fetched per row. Every candidate must pass the full verification policy before it can become an output route; raising this raises recall and run time, never the risk of an unproven URL.

## `maxBridgePages` (type: `integer`):

Upper bound on job-mirror pages consulted per row. Mirrors may reveal an official outbound URL to verify, but a mirror itself can never become the published route.

## Actor input object example

```json
{
  "rows": [
    {
      "jobId": "4441060956",
      "jobUrl": "https://www.linkedin.com/jobs/view/account-executive-travel-at-nike-communications-inc-4441060956",
      "jobTitle": "Account Executive - Travel",
      "companyName": "Nike Communications, Inc.",
      "companyUrl": "https://www.linkedin.com/company/nike-communications",
      "location": "New York, NY"
    },
    {
      "jobId": "4440198123",
      "jobUrl": "https://www.linkedin.com/jobs/view/national-account-manager-at-sprout-living-4440198123",
      "jobTitle": "National Account Manager",
      "companyName": "Sprout Living",
      "companyUrl": "https://www.linkedin.com/company/sprout-living",
      "location": "United States"
    }
  ],
  "maxItems": 25,
  "autoDiscoverCompanyWebsite": true,
  "useReaderFallback": true,
  "maxSearchQueries": 3,
  "maxCandidates": 6,
  "maxBridgePages": 3
}
```

# Actor output Schema

## `jobResults` (type: `string`):

No description

## `summary` (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 = {
    "rows": [
        {
            "jobId": "4441060956",
            "jobUrl": "https://www.linkedin.com/jobs/view/account-executive-travel-at-nike-communications-inc-4441060956",
            "jobTitle": "Account Executive - Travel",
            "companyName": "Nike Communications, Inc.",
            "companyUrl": "https://www.linkedin.com/company/nike-communications",
            "location": "New York, NY"
        },
        {
            "jobId": "4440198123",
            "jobUrl": "https://www.linkedin.com/jobs/view/national-account-manager-at-sprout-living-4440198123",
            "jobTitle": "National Account Manager",
            "companyName": "Sprout Living",
            "companyUrl": "https://www.linkedin.com/company/sprout-living",
            "location": "United States"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kamerozkan/linkedin-job-apply-link-verifier").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 = { "rows": [
        {
            "jobId": "4441060956",
            "jobUrl": "https://www.linkedin.com/jobs/view/account-executive-travel-at-nike-communications-inc-4441060956",
            "jobTitle": "Account Executive - Travel",
            "companyName": "Nike Communications, Inc.",
            "companyUrl": "https://www.linkedin.com/company/nike-communications",
            "location": "New York, NY",
        },
        {
            "jobId": "4440198123",
            "jobUrl": "https://www.linkedin.com/jobs/view/national-account-manager-at-sprout-living-4440198123",
            "jobTitle": "National Account Manager",
            "companyName": "Sprout Living",
            "companyUrl": "https://www.linkedin.com/company/sprout-living",
            "location": "United States",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kamerozkan/linkedin-job-apply-link-verifier").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 '{
  "rows": [
    {
      "jobId": "4441060956",
      "jobUrl": "https://www.linkedin.com/jobs/view/account-executive-travel-at-nike-communications-inc-4441060956",
      "jobTitle": "Account Executive - Travel",
      "companyName": "Nike Communications, Inc.",
      "companyUrl": "https://www.linkedin.com/company/nike-communications",
      "location": "New York, NY"
    },
    {
      "jobId": "4440198123",
      "jobUrl": "https://www.linkedin.com/jobs/view/national-account-manager-at-sprout-living-4440198123",
      "jobTitle": "National Account Manager",
      "companyName": "Sprout Living",
      "companyUrl": "https://www.linkedin.com/company/sprout-living",
      "location": "United States"
    }
  ]
}' |
apify call kamerozkan/linkedin-job-apply-link-verifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kamerozkan/linkedin-job-apply-link-verifier",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TW4QDjkomVJH1SPsd/builds/Jjwha63aFYGbA9FxP/openapi.json
