# Indeed & LinkedIn Job Scraper + Change Tracking (`solidscrape/job-intelligence-api`) Actor

Scrape Indeed and LinkedIn jobs or public Greenhouse employer boards into one API. Keep salary currencies and pay periods, remove exact duplicates, and track new and changed jobs. Start with a five-job example, clear pricing and spending caps. Apify free-plan credit can be used.

- **URL**: https://apify.com/solidscrape/job-intelligence-api.md
- **Developed by:** [Shahryar](https://apify.com/solidscrape) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 unique job checkeds

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

## Indeed & LinkedIn Job Scraper and Job Change Tracking API

Job Intelligence API searches Indeed and LinkedIn, optionally adds public Greenhouse employer boards, and returns a consistent job feed with salary fields, original source links, exact-match deduplication and observed changes. Use it for recruiting research, job-board feeds and employer hiring monitoring. No LinkedIn login or personal cookies are required. Search-source availability can change; failed requests are reported explicitly.

### Try five jobs first

1. Select **Try for free** or **Try it** on this Actor's Store page and sign in to Apify.
2. Use the small Indeed example below. Set the platform maximum charge to **$0.05** as an additional guard.
3. Select **Start**, then open the **Job dataset** output. Export the results as JSON, CSV or Excel.
4. Open **Run summary and estimated charges** to see checked-job counts, source failures and charges before increasing your limits.

```json
{
 "sources": ["indeed"],
 "searchQueries": ["software engineer"],
 "location": "New York, NY",
 "country": "us",
 "maxItems": 5,
 "maxSourcePages": 1,
 "includeLinkedInDetails": false,
 "maxSpendUsd": 0.05
}
```

One source request and five checked jobs cost **$0.026** at up to 1 GB. The $0.05 setting is a cap, not a fixed charge. A source can return fewer jobs or fail; source-attempt fees still apply.

#### Can I try it without paying for an Apify plan?

Yes. Apify's [Free plan](https://apify.com/pricing) currently includes **$5 of monthly platform credit**, shared across the Apify Store and your own Actors, with no credit card required. This Actor supports free-plan users with the same features and event prices. Runs consume your available Apify credit; this is not an unlimited free API or a separate Solidscrape coupon. Check your remaining credit and Apify's current terms before running.

The small example above lets you evaluate real output while keeping a clear spending cap. No external account, personal cookies or third-party API key is needed in the input.

### Combine Indeed and LinkedIn jobs

```json
{
 "sources": ["indeed", "linkedin"],
 "searchQueries": ["software engineer"],
 "location": "New York, NY",
 "country": "us",
 "maxItems": 20,
 "maxSourcePages": 2,
 "maxSpendUsd": 0.10
}
```

Two source requests and 20 checked jobs cost **$0.061**, without optional LinkedIn details. Source rankings vary, so this is a bounded sample, not complete market coverage.

### Get jobs directly from a Greenhouse employer board

```json
{
 "sources": ["greenhouse"],
 "employerBoards": [{"slug":"figma", "company":"Figma"}],
 "searchQueries": ["engineer"],
 "maxItems": 10,
 "maxSourcePages": 1,
 "maxSpendUsd": 0.10
}
```

Greenhouse company names are the labels you supply with each board. Board keywords match job titles, avoiding unrelated roles that mention a skill in their description. Location filters apply to search providers, not employer boards. The Actor does not enumerate all companies automatically.

### What makes this useful

- Consistent job, company, location and salary fields across sources, with original source links and per-source identifiers.
- Salary ranges preserve currency, pay period and estimated status when supplied. Hourly pay is not guessed into annual pay. Missing salary fields stay unknown; original salary text is retained. Greenhouse pay ranges retain their source labels in `salaryRanges`; multiple regional or job-level ranges are not collapsed into one misleading range.
- Exact source IDs and job-specific application URLs support deduplication. Similar title/company/location combinations receive a possible-duplicate group ID but remain separate jobs.
- LinkedIn descriptions are optional because they require extra requests. Indeed and employer-board descriptions are included when available; text is capped at 6,000 characters. A normalized full-text hash supports description-change detection.
- Source request diagnostics, partial comparison status, clear spending limits and observation timestamps.

### Monitor changes

Pass a previous run's default key-value store ID as `previousSnapshotStoreId`, keeping the same searches, sources, coverage limits and detail settings. Enable `changesOnly` to emit only new and updated records. The first run is a `baseline`, not evidence that those jobs were newly posted.

For a first monitoring workflow:

1. Run the Greenhouse example above and wait for it to succeed.
2. Copy that run's **default key-value store ID** from its Storage tab. You need the store ID, not its run ID or dataset ID.
3. Run the same input again, adding the two fields below. Replace the placeholder with your own first run's store ID.

```json
{
 "sources": ["greenhouse"],
 "employerBoards": [{"slug":"figma", "company":"Figma"}],
 "searchQueries": ["engineer"],
 "maxItems": 10,
 "maxSourcePages": 1,
 "maxSpendUsd": 0.10,
 "previousSnapshotStoreId": "YOUR_PREVIOUS_KVS_ID",
 "changesOnly": true
}
```

An empty dataset on the second run can be correct: inspect `SUMMARY.counts.unchanged` and `SNAPSHOT`. Unchanged jobs were still checked and billed. For repeated comparisons, use the most recent successful run's store ID each time; a fixed old baseline always compares against that older observation. No schedule is created automatically.

All unique jobs checked are saved in `SNAPSHOT` and billed, including unchanged jobs suppressed from the dataset. Up to 500 jobs are retained in history so one missing search sample does not reset their first-seen dates. `observedInRun` and `currentJobIds` distinguish fresh observations from carried history. Older records can be evicted at the history limit, and `historyEvictions` reports that count.

`NOT_SEEN` means a job was absent from this bounded sample. It **does not mean expired or closed**. Lifecycle changes require explicit evidence: a source-declared deadline that has passed, or a checked detail page explicitly stating that applications are closed. A 403, 404, failed search, or missing record is not closure evidence. Lifecycle is `unknown` when this evidence is unavailable.

Changes compare title, company, location, workplace, employment type, structured salary, application URL, explicit lifecycle evidence and description hash when available. Missing fields are not interpreted as deletions. Changes are observations from upstream sources, not independently verified hiring decisions.

### Pricing

| Event | USD | Meaning |
|---|---:|---|
| Actor start | $0.001 per allocated GB, minimum one | Startup |
| Source request | $0.02 | One search page or employer-board request attempt, including empty, blocked or unrecognized responses |
| Unique job checked | $0.001 | One unique job saved in SNAPSHOT and compared, including unchanged jobs |
| Optional LinkedIn detail attempt | $0.003 | One selected job-detail attempt, including blocked, missing or unrecognized responses |

Example: two source pages and 20 unique checked jobs cost $0.061 at up to 1 GB, without optional details. Ten LinkedIn detail attempts add $0.03. Results and optional detail availability are not guaranteed by the fee.

There are no filter surcharges. Compute and residential search traffic are included. The lower of the input and platform event-spending caps applies. Work stops before known event costs exceed the budget. Source requests include up to three zero-byte connection attempts within 30 seconds; responses are not automatically retried. Maximum allocated memory is 1 GB.

### Outputs

The **Job intelligence** view shows the main fields. Use **Salary comparison** for pay data and **Observed job changes** for change type, changed fields and first/last-seen timestamps.

This abbreviated example is illustrative; it is not a current vacancy or a promise of which fields every source will supply:

```json
{
 "jobId": "indeed:example-posting",
 "title": "Software Engineer",
 "company": "Example Company",
 "location": "New York, NY",
 "workplace": null,
 "salary": {
   "min": 110000, "max": 140000,
   "currency": "USD", "period": "year",
   "isEstimated": false, "annualized": false
 },
 "applicationUrl": "https://example.com/jobs/12345678",
 "change": {"type":"baseline", "fields":[], "comparisonStatus":"partial"},
 "lifecycle": "unknown"
}
```

- Default dataset: normalized job records, optionally filtered to changes.
- `SNAPSHOT`: checked jobs and retained history for the next run.
- `SUMMARY`: counts, stop reason and estimated charges.
- `SOURCE_REPORT` and `PAGE_*`: source attempts and normalized candidate records. Source-page work may be billed even if a run stops before final job output is produced.
- `DETAIL_REPORT` and `JOB_*`: optional detail attempts and saved job records.
- `NOT_SEEN`: absent prior records, without inferred closure.
- `PROGRESS`: checkpoints; uncertain billing outcomes stop rather than being charged twice.

Save previous snapshots in retained storage if they are needed beyond your account's retention period. Each user's baseline comes from their selected storage; no cross-customer history is shared.

### API

POST the quick-start JSON to `https://api.apify.com/v2/acts/solidscrape~job-intelligence-api/runs` with your Apify bearer token in the authorization header. Poll the run ID, then read its default dataset and the output records above. Secrets are not required in input.

For a small synchronous request in JavaScript (Node.js 22+), using your own token from an environment variable:

```javascript
const response = await fetch(
  'https://api.apify.com/v2/acts/solidscrape~job-intelligence-api/run-sync-get-dataset-items?memory=512&timeout=120&maxTotalChargeUsd=0.05',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      sources: ['indeed'], searchQueries: ['software engineer'],
      location: 'New York, NY', maxItems: 5,
      maxSourcePages: 1, maxSpendUsd: 0.05
    })
  }
);
if (!response.ok) throw new Error(`Apify request failed: ${response.status}`);
const jobs = await response.json();
console.log(jobs);
```

Use asynchronous `/runs` for larger jobs and monitoring so you retain the run's store ID. The synchronous endpoint above returns dataset rows only. After-run storage access can incur normal Apify storage charges; retain snapshots only as long as your workflow needs them.

### Frequently asked questions

#### Does this scrape LinkedIn without logging in?

Yes. It reads public job listings without personal cookies. It does not access private profiles or guarantee access to every LinkedIn job. Search cards provide basic fields; enable `includeLinkedInDetails` when you need available descriptions and employment types, at $0.003 per detail attempt.

#### Are salary amounts comparable across sources?

Compare currency and pay period first. The Actor keeps hourly, monthly and annual amounts distinct and does not guess annual pay. LinkedIn often supplies salary text without a structured range. Missing structured salaries stay null. Multiple employer pay bands retain their labels in `salaryRanges`.

#### Why did a changed-only run return zero rows?

All observed jobs may be unchanged. Check `SUMMARY` and `SNAPSHOT` before treating an empty dataset as a failure. If no source succeeded, inspect `SOURCE_REPORT`; the run reports failed requests explicitly.

#### Does a missing job mean it expired?

No. A bounded search can omit a still-open job. `NOT_SEEN` records absence; expiration or closure requires a source deadline or explicit closure evidence.

#### Which Actor should I choose for LinkedIn alone?

Choose this Actor when combining sources or comparing snapshots matters. In one ten-job comparison, a dedicated LinkedIn competitor was faster and cheaper for LinkedIn details alone. This Actor does not claim to be the cheapest or fastest for every single-source workload.

#### What should I include when reporting a problem?

Use this Actor's Issues tab. Include the affected source, run ID, expected behavior and the relevant `SOURCE_REPORT` error. Never post API tokens, cookies or private customer data. Share a debug run only if you are comfortable with the data it contains.

### Practical limits

Up to five search queries, five employer boards, 20 source pages, 500 collected candidates and 250 unique checked jobs per run. Sources are interleaved for a balanced sample; search rankings and coverage can vary. Narrow searches are recommended. This is not an exhaustive job-market census.

LinkedIn search cards often omit salary or descriptions. Optional detail requests can improve fields but can also be blocked. Workplace type is derived only from explicit location labels such as Remote or Hybrid; a mention of remote work in an unrelated description does not establish a fully remote role. No application forms are submitted, and no candidate profiles or private account data are collected.

This independently developed Actor is provided by Solidscrape. LinkedIn, Indeed and Greenhouse are the respective source providers, not publishers or endorsers of this Actor.

# Actor input Schema

## `sources` (type: `array`):

Search Indeed, LinkedIn, or explicitly selected Greenhouse employer boards. Each source request costs $0.02, including empty or failed attempts.

## `searchQueries` (type: `array`):

Up to five job searches. Required for Indeed and LinkedIn. On employer boards, a case-insensitive match is applied to the job title.

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

Free-text location for Indeed and LinkedIn, for example New York, NY. Employer-board results are not location-filtered.

## `country` (type: `string`):

Two-letter country code used for Indeed and residential search proxy routing, for example us or gb.

## `employerBoards` (type: `array`):

Required when greenhouse is selected. Each entry contains a public board slug and a company label, for example {"slug":"figma","company":"Figma"}. Up to five boards. Company labels are supplied by you.

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

Hard cap across all sources, after exact-match deduplication. Results interleave sources so one source does not consume the entire sample. Maximum 250.

## `maxSourcePages` (type: `integer`):

Hard request cap across searches and employer boards. $0.02 per completed attempt. Up to 500 candidates are considered before deduplication.

## `includeDescriptions` (type: `boolean`):

Include descriptions where already supplied by a source. Returned text is capped at 6,000 characters, with a normalized full-text hash. LinkedIn search cards need the separate detail option for full descriptions.

## `includeLinkedInDetails` (type: `boolean`):

Optional $0.003 per selected LinkedIn job-detail attempt, including blocked, missing or unrecognized pages. Can supply descriptions, employment type and explicit closure evidence. Disabled by default.

## `previousSnapshotStoreId` (type: `string`):

Select a previous run's default key-value store with identical source, search, coverage and detail settings. Only read access is requested.

## `changesOnly` (type: `boolean`):

Requires previousSnapshotStoreId. Unchanged jobs are still checked, billed and saved in SNAPSHOT. Jobs absent from search are recorded separately as not_seen, never automatically expired.

## `maxSpendUsd` (type: `number`):

The lower of this limit and the platform spending cap applies. Includes startup, source requests, unique jobs checked and optional details.

## `maxRunSeconds` (type: `integer`):

Stop new work at this deadline and save partial results. Network requests also have individual time and response-size limits.

## Actor input object example

```json
{
  "sources": [
    "indeed"
  ],
  "searchQueries": [
    "software engineer"
  ],
  "location": "New York, NY",
  "country": "us",
  "maxItems": 5,
  "maxSourcePages": 1,
  "includeDescriptions": true,
  "includeLinkedInDetails": false,
  "changesOnly": false,
  "maxSpendUsd": 0.05,
  "maxRunSeconds": 300
}
```

# Actor output Schema

## `jobs` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `snapshot` (type: `string`):

No description

## `sourceReport` (type: `string`):

No description

## `detailReport` (type: `string`):

No description

## `notSeen` (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 = {
    "sources": [
        "indeed"
    ],
    "searchQueries": [
        "software engineer"
    ],
    "location": "New York, NY",
    "maxItems": 5,
    "maxSourcePages": 1,
    "maxSpendUsd": 0.05
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidscrape/job-intelligence-api").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 = {
    "sources": ["indeed"],
    "searchQueries": ["software engineer"],
    "location": "New York, NY",
    "maxItems": 5,
    "maxSourcePages": 1,
    "maxSpendUsd": 0.05,
}

# Run the Actor and wait for it to finish
run = client.actor("solidscrape/job-intelligence-api").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 '{
  "sources": [
    "indeed"
  ],
  "searchQueries": [
    "software engineer"
  ],
  "location": "New York, NY",
  "maxItems": 5,
  "maxSourcePages": 1,
  "maxSpendUsd": 0.05
}' |
apify call solidscrape/job-intelligence-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,solidscrape/job-intelligence-api"
        }
    }
}
```

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/qKTadgYLfUnt5TQea/builds/6lpf7Nz7eO2GQ3vMR/openapi.json
