# Indeed Jobs Scraper (`scrapyx/indeed-jobs-scraper`) Actor

Scrapes live job listings from Indeed across 21 country sites: title, company, rating, location, parsed salary range, job types, remote model, benefits and posting age. Sponsored ads are flagged and excluded by default, and each summary row reports whether your location filter really applied.

- **URL**: https://apify.com/scrapyx/indeed-jobs-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 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.
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

## Indeed Jobs Scraper (21 countries)

Scrapes live job listings from **[Indeed](https://www.indeed.com)** across
**21 country sites** — US, UK, Canada, Australia, Germany, France, India,
Singapore, Netherlands, Spain, Italy, Brazil, Japan, Mexico, Ireland,
Philippines, South Africa, UAE, Poland, Sweden and Switzerland.

Public data only. No login, no cookies, no browser, HTTP-only. There is no bot
challenge to bypass here — every TLS profile tried returned the full page cold,
with no warm-up and no session.

### What makes this one different: it tells you when it is missing data

Indeed has three ways of handing you a result that *looks* right and is not.
This actor is built around all three, and reports each one in the data rather
than hiding it.

#### 1. It respects Indeed's stated crawl depth — and says so

Indeed's `robots.txt` names the Anthropic agents explicitly and permits
`&start=0` through `&start=90`, while disallowing deeper paging. That is
**10 pages, roughly 150 organic jobs per query**, and this actor stops there
even though deeper pages do answer.

That ceiling is the honest headline limit. To cover a large board, **split
into narrower queries** — per city, per job type, per `datePosted` window —
rather than paging deeper. Every summary row carries `totalJobCount`,
`estimatedReachable` and `robotsPageCeilingReached` so you always know how much
of the result set you actually have.

The same policy also means there is **no job-description pass**: `/viewjob` is
disallowed for us. Job rows carry a `jobUrl` you can open yourself, but the
actor never fetches it.

#### 2. A mistyped location does not fail — it silently returns the whole country

Searching `Austin, TX` returns Austin jobs. Searching `Zzzqqxx, ZZ` returns
**11,078 matching jobs from Denver, Charlotte, Los Angeles, Philadelphia and
Atlanta** — and Indeed echoes your typo straight back as the search location,
so upstream's own response cannot be trusted to catch it.

Every summary row therefore carries **`locationMatchRatio`**: the fraction of
returned jobs that are actually near what you asked for. `1.0` means the filter
applied; `0.0` with `locationLooksIgnored: true` means Indeed dropped it and
served the nation. The run logs a warning naming the problem.

#### 3. Most of page one is advertising

On the US site, page 1 returns 40 cards — **25 of them paid placements**, not
organic results. Pages 2–10 return 15 cards each, all organic.

`includeSponsored` is therefore **off by default**. Sponsored cards are still
counted, so the summary always tells you the split (`organicCount` vs
`sponsoredCount`), and when you do include them every row carries
`isSponsored`.

#### Plus: an intermittent sign-in wall, handled rather than ignored

Roughly **20% of page-2-and-beyond requests** come back as HTTP 200 with a
"create an account or sign in" page and no jobs at all. It is probabilistic —
the same URL usually succeeds on retry. This actor retries it with backoff, and
if a page never clears, records it in **`loginWalledPages`** instead of quietly
returning a short result set.

### Bad filter values are refused before the run starts

Indeed does not reject filter values it does not understand — it **ignores**
them and returns unfiltered results. Measured against a 3,386-job baseline:

| You send | Indeed does | This actor does |
| --- | --- | --- |
| `jobType=zzzz` | returns 3,362 — the baseline | refuses, naming the valid values |
| `datePosted=99` | returns the baseline | refuses |
| `sort=zzz` | returns the baseline, in relevance order | refuses |
| `radius=7` | not honoured predictably | refuses (Indeed's own steps only) |
| `maxPagesPerQuery=11` | would answer | refuses — past the robots ceiling |

Checked in code, not only in the input schema, so an API or CLI run gets the
same protection as a Console run.

### What you get

Three record types share one dataset, told apart by `recordType`.

#### `JOB` — one row per vacancy

Indeed's own card object passed through verbatim (~117 fields), plus our
envelope. Highlights: job title, company, company rating and review count,
formatted location, city/state, **parsed salary range with period**
(`extractedSalary`), salary text, job types, remote-work model, ranked
benefits, taxonomy attributes, snippet, posting age, apply flags, `jobkey` and
a `jobUrl`.

One upstream quirk worth knowing: the top-level `jobTypes` array is often
empty even when the job *is* full-time — the real value lives in
`taxonomyAttributes` under the `job-types` label. Both are passed through
untouched.

#### `SEARCH_SUMMARY` — one row per query

Indeed's own `totalJobCount`, rows returned, organic/sponsored split, pages
fetched, `loginWalledPages`, `locationMatchRatio`, `robotsPageCeilingReached`
and `estimatedReachable`.

#### `ERROR` — one row per query that failed

So every input maps to at least one row.

### Filters

`searchQueries` (fanned out independently), `country`, `location`, `jobType`,
`datePosted` (1/3/7/14 days), `radius`, `sort` (relevance or date),
`remoteOnly`, `includeSponsored`, `maxItems`, `maxPagesPerQuery`,
`minRequestInterval`, `maxConcurrency`.

All of them were verified to actually move Indeed's result count — see
`CRAWLING_METHOD.md` for the measurements.

`remoteOnly` uses Indeed's own Remote facet and is **very** aggressive: a query
matching 3,386 jobs dropped to 8 with it on.

### Known limits

- **~150 organic jobs per query**, by Indeed's stated policy, not by choice.
- **No job descriptions** — the detail page is robots-disallowed for us. The
  `snippet` field carries Indeed's own summary.
- **No company-profile pass** — `/company/*` is disallowed too.
- Non-English country sites want queries in the local language.
- Indeed's `totalJobCount` wobbles by ~1% between identical requests; it is
  Indeed's estimate, passed through as-is.

### Proxy — please read before running

**This actor needs a residential proxy to be reliable.** Indeed gates on IP
reputation, not on browser fingerprint: from a residential exit it answers 200
on every request with no challenge at all, while much of a datacentre proxy
pool gets 403.

Measured on the Apify platform against a US datacentre pool: `uk`, `au` and
`de` came through, `www.indeed.com` (US) came through intermittently, and `in`
and `ca` did not come through at all. The actor rotates its exit IP on every
retry (up to 12 attempts) because some IPs in a pool are accepted and some are
not — but rotation cannot manufacture a clean IP if the pool has none.

- **With Apify Residential proxy** — recommended, and what the default input
  selects.
- **With datacentre proxy** — expect intermittent `fetch_failed` rows,
  especially on the US, India and Canada hosts.
- **With no proxy on the platform** — will not work at all. Apify containers
  have no direct outbound route; `curl` fails to connect in milliseconds.

Pass an empty `proxyConfiguration` (`{}`) only for local runs.

# Actor input Schema

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

Job titles or keywords, e.g. "python developer". Each query is crawled independently and gets its own summary row. Leave empty to crawl the unfiltered board for the location and filters below.

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

Which Indeed country site to search. Each country is a separate job board with its own listings; use the local language in your query for non-English sites.

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

City, region or postcode, e.g. "Austin, TX" or "London". IMPORTANT: Indeed does not reject a place name it cannot resolve - it drops the filter and returns the whole country while echoing your text back. Every summary row carries locationMatchRatio so you can see whether the filter actually applied.

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

Employment type filter. Leave empty for any. Unknown values are silently ignored by Indeed, so this actor refuses anything outside the list.

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

Only jobs posted in the last N days. Leave empty for any age.

## `radius` (type: `string`):

Distance around the location. Indeed's own radio buttons only - other values are not honoured predictably. Leave empty for Indeed's default (25).

## `sort` (type: `string`):

Result ordering. Relevance is Indeed's default; date puts the newest postings first.

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

Apply Indeed's own Remote facet. Be aware this is a very aggressive filter - a query matching 3,386 jobs dropped to 8 with it on.

## `includeSponsored` (type: `boolean`):

Off by default. On the US site, 25 of page 1's 40 cards are paid placements rather than organic search results. They are always counted in the summary row; this switch decides whether they also become job rows.

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

Stop after this many job rows per search query. Set 0 for no cap (still bounded by the page ceiling below).

## `maxPagesPerQuery` (type: `integer`):

Search result pages to fetch per query, 1-10. Ten is a hard ceiling, not a default: Indeed's robots.txt permits AI agents start=0 through start=90 and disallows deeper paging. That is roughly 150 organic jobs per query - split into narrower queries to cover more.

## `minRequestInterval` (type: `string`):

Politeness pacing between request starts. This, not maxConcurrency, is the honest speed control - once the rate cap binds, extra concurrency buys nothing.

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

Upper bound on in-flight requests across all queries.

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

Apify Proxy settings. Residential is recommended on the platform; pass an empty object to go direct.

## Actor input object example

```json
{
  "searchQueries": [
    "python developer"
  ],
  "country": "us",
  "location": "Austin, TX",
  "jobType": "",
  "datePosted": "",
  "radius": "",
  "sort": "",
  "remoteOnly": false,
  "includeSponsored": false,
  "maxItems": 100,
  "maxPagesPerQuery": 10,
  "minRequestInterval": "1.2",
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "searchQueries": [
        "python developer"
    ],
    "location": "Austin, TX"
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/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 = {
    "searchQueries": ["python developer"],
    "location": "Austin, TX",
}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/indeed-jobs-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 '{
  "searchQueries": [
    "python developer"
  ],
  "location": "Austin, TX"
}' |
apify call scrapyx/indeed-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/indeed-jobs-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/TwYT9CtX7brx6ptVT/builds/D1y9Rl6rkqE9wJJqX/openapi.json
