# Indeed Jobs Scraper — salary & posting age as numbers (`henrycrawl/indeed-jobs-scraper`) Actor

Scrape Indeed search results into typed rows: salary parsed into min, max, period and a yearly estimate, posting age as a number of days, company rating, benefits and remote flag. Includes a hard cap on results so a run can never bill you more than you asked for.

- **URL**: https://apify.com/henrycrawl/indeed-jobs-scraper.md
- **Developed by:** [Henry Crawl](https://apify.com/henrycrawl) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 job postings

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

## Indeed Jobs Scraper — salary, posting age and rating as numbers

Scrape Indeed search results into rows you can sort and filter immediately. Salary
arrives as `salaryMin`, `salaryMax`, `salaryPeriod` and a yearly estimate, posting age
as a number of days, and company rating as a number — not as English strings you have
to parse yourself.

### What you get per job

| Field | Example | Notes |
|---|---|---|
| `jobKey` | `d8123dc510d5b757` | Indeed's own id, use it to deduplicate across runs |
| `title` | `Warehouse Associate - Day Shift` | |
| `company` | `Berlin Packaging` | |
| `companyRating` | `3.8` | Parsed to a number when Indeed shows it |
| `location` | `Chicago, IL` | |
| `isRemote` | `false` | |
| `salaryText` | `$18 - $22 an hour` | Original string, kept for reference |
| `salaryMin` / `salaryMax` | `18` / `22` | Numbers |
| `salaryPeriod` | `hour` | `hour`, `day`, `week`, `month`, `year` |
| `salaryCurrency` | `USD` | |
| `salaryYearlyEstimate` | `41600` | Normalised so hourly and yearly postings are comparable |
| `jobType` | `Full-time` | |
| `benefits` | `["Health insurance","401(k)"]` | The benefit chips Indeed shows on the card |
| `postedDaysAgo` | `3` | `0` means posted today |
| `postedDateApprox` | `2026-07-31` | Derived from the posting age |
| `isUrgentlyHiring`, `isEasyApply` | `true` | |
| `snippet` | | Short description shown on the card |
| `jobUrl` | | Direct link to the posting |
| `searchQuery`, `searchLocation`, `pageNumber` | | Provenance of every row |
| `indeedDomain`, `proxyCountry`, `scrapedAt` | | The exact context the data was captured in |

### Input

```json
{
  "queries": ["warehouse associate", "forklift operator"],
  "location": "Chicago, IL",
  "indeedDomain": "www.indeed.com",
  "proxyCountry": "US",
  "datePosted": "last_3_days",
  "jobType": "full_time",
  "maxJobs": 1000
}
```

Or paste Indeed search URLs directly in `startUrls`.

Filters available: country site, location and radius, date posted, job type, remote
only, minimum salary, and sorting by relevance or date.

### Why salary comes back as numbers

A job feed is only useful if you can rank it. Hourly and yearly postings are not
comparable as strings, so `salaryYearlyEstimate` converts both to the same basis
(hourly × 2,080, weekly × 52, monthly × 12). The original `salaryText` is always kept
so you can audit any conversion.

### Country matters

Indeed is country-specific in two independent ways. `indeedDomain` picks which
national site to search, and `proxyCountry` decides where the request appears to come
from — which affects both results and currency. They default to the United States.

Residential proxies are used by default: Indeed refuses datacenter IP ranges.

### The result cap is real

`maxJobs` stops the run at exactly that number. The run also stops on its own if three
consecutive pages return nothing usable, so a layout change cannot quietly turn into a
long, expensive run.

### Runs fail loudly, not silently

Every run writes a `DIAGNOSTICS` record with the block rate, bytes per item and the
fill rate of every output field:

```json
{ "itemsExtracted": 500, "blockRate": "0%", "fieldCoverage": { "salaryMin": "62%", "company": "100%" } }
```

If nothing was extracted, or more than half the pages were blocked, the run is marked
**failed** with the reason. An empty column that looks like data is worse than an error.

### Typical uses

- Salary benchmarking by role, city and employer
- Tracking which companies are hiring, and how fast their postings turn over
- Feeding a niche job board or a recruiting pipeline
- Labour market research on posting volume and pay trends

### Notes and limits

- `salaryMin`, `companyRating` and `benefits` only appear where Indeed shows them, and
  are reported as `null` elsewhere rather than guessed.
- `postedDateApprox` is derived from a relative age ("3 days ago"), so it is accurate
  to the day, not the hour.
- Keep `maxConcurrency` low. Indeed authorises by session.

### Support

If a field stops filling or you need a filter that is not exposed, open an issue on the
actor page with the input you used.

# Actor input Schema

## `queries` (type: `array`):

Each term runs as its own search using the filters below. Leave empty if you paste URLs instead.

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

City, state or postal code, exactly as you would type it on Indeed. Leave empty to search the whole country.

## `startUrls` (type: `array`):

Paste Indeed search URLs. Scraped in addition to any search terms above.

## `indeedDomain` (type: `string`):

Which national Indeed to search. Independent from Proxy country.

## `proxyCountry` (type: `string`):

Indeed serves different results and currencies depending on where the request comes from. Match this to the market you care about.

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

The run stops at exactly this many jobs. Enforced, not a suggestion, so you cannot be billed for more rows than you asked for.

## `datePosted` (type: `string`):

Only include postings published within this window.

## `jobType` (type: `string`):

Employment type filter, same options Indeed offers.

## `remoteOnly` (type: `boolean`):

Restrict results to postings Indeed marks as remote.

## `sortBy` (type: `string`):

Relevance is Indeed's default ranking; date returns the newest postings first.

## `radiusKm` (type: `integer`):

Distance around the location, in the unit Indeed uses for that country.

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

Yearly salary floor, applied as Indeed's own salary filter.

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

Keep this low. Indeed gates by session, so parallelism burns sessions faster than it saves time.

## `debugRawText` (type: `boolean`):

Adds the raw text of each result card to the output, for diagnosing a field that stopped filling.

## `proxyConfiguration` (type: `object`):

Defaults to Apify residential proxy in the selected country. Indeed refuses datacenter IP ranges.

## Actor input object example

```json
{
  "queries": [
    "warehouse associate"
  ],
  "location": "Chicago, IL",
  "startUrls": [],
  "indeedDomain": "www.indeed.com",
  "proxyCountry": "US",
  "maxJobs": 500,
  "datePosted": "any",
  "jobType": "any",
  "remoteOnly": false,
  "sortBy": "relevance",
  "maxConcurrency": 2,
  "debugRawText": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One row per Indeed posting: title, company, company rating, location, remote flag, salary parsed into min, max, period, currency and a yearly estimate, job type, benefits, posting age in days, and the posting URL.

## `diagnostics` (type: `string`):

Block rate, pages processed, bytes per item and the fill rate of every output field for this run.

# 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 = {
    "queries": [
        "warehouse associate"
    ],
    "location": "Chicago, IL"
};

// Run the Actor and wait for it to finish
const run = await client.actor("henrycrawl/indeed-jobs-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 = {
    "queries": ["warehouse associate"],
    "location": "Chicago, IL",
}

# Run the Actor and wait for it to finish
run = client.actor("henrycrawl/indeed-jobs-scraper").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 '{
  "queries": [
    "warehouse associate"
  ],
  "location": "Chicago, IL"
}' |
apify call henrycrawl/indeed-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=henrycrawl/indeed-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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