# Indeed Jobs Scraper — Listings, Pay & Employers (`thenetaji/indeed-jobs-scraper`) Actor

Turn a list of roles and cities into a spreadsheet of live openings. Each row carries the employer and its rating, the pay range already parsed into numbers, the location, the posting date, the apply link, and whether the employer paid for the placement.

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

## Pricing

from $3.40 / 1,000 jobs

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

Search Indeed by role and city and export every matching job as a spreadsheet row. Each row
carries the job title, the employer and its rating, the pay range already parsed into
numbers, the location split into city and state, the posting date, the apply link, and
whether the employer paid for the placement.

A run takes a list of search terms and a list of locations, and searches every term against
every location. Four terms and five cities is twenty searches in one run.

### Accepted input

`queries` is required and is a list of search terms, one per line — a role, a skill, or an
employer name. `locations` is a list of places, given as a city and state (`Austin, TX`), a
state, a postcode or `remote`; leave it empty for one nationwide search per term.

`radius` sets how many miles around each location to include, up to `100`. `days_ago`
restricts results to postings first indexed within `1`, `3`, `7` or `14` days. `job_type`
selects `fulltime`, `parttime`, `contract`, `temporary` or `internship`. `sort` is either
`relevance`, which is the site's own default ordering, or `date`.

`maxItems` bounds the rows saved across the whole run and defaults to `100`; a value of `0`
removes the bound.

```json
{
  "queries": ["software engineer", "backend developer"],
  "locations": ["Austin, TX", "Dallas, TX"],
  "days_ago": "7",
  "job_type": "fulltime",
  "maxItems": 100
}
```

### Response fields

```json
{
  "search_query": "engineer",
  "search_location": "Austin, TX",
  "job_key": "c9827654f65af6b6",
  "title": "Camera Optomechanical Engineer",
  "normalized_title": "Engineer",
  "company_name": "Saronic Technologies",
  "company_rating": 3.9,
  "company_review_count": 1204,
  "company_slug": "Saronic-Technologies",
  "company_id": "6b92e3265b43baa6",
  "location_formatted": "Austin, TX",
  "city": "Austin",
  "state": "TX",
  "country": "US",
  "salary_min": 145000,
  "salary_max": 210000,
  "salary_period": "YEARLY",
  "salary_text": "$145,000 - $210,000 a year",
  "job_types": ["Full-time"],
  "attributes": { "job-types": ["Full-time"], "benefits": ["401(k)", "Paid time off"] },
  "created_at": "2026-02-20T04:32:27.426Z",
  "posted_relative": "30+ days ago",
  "sponsored": true,
  "ad_bid": 586719,
  "ad_bid_position": 3,
  "job_url": "https://www.indeed.com/viewjob?jk=c9827654f65af6b6"
}
```

Fields absent from a posting are returned as null rather than omitted, so every row has the
same shape.

`salary_min` and `salary_max` are numbers, not the string on the card, and `salary_period`
says what period they are quoted over. This matters more than it sounds: hourly and salaried
roles sit side by side in one result set, so two rows are only comparable once the period is
read. `salary_text` keeps the string exactly as the card rendered it.

`search_query` and `search_location` record which of the run's searches found the row. In a
run over twenty searches they are the difference between a dataset that can be grouped by
market and one that cannot.

`sponsored`, `ad_bid` and `ad_bid_position` describe the advertising behind a placement:
whether the employer paid for it, the bid, and where that bid placed the row. That makes
"which employers are buying visibility for this search, and at what rank" an answerable
question, and it is worth reading `sponsored` before treating result order as a ranking.

`company_slug` is the exact value [Indeed Company Scraper](https://apify.com/thenetaji/indeed-company-scraper)
accepts, and `job_key` is the exact value
[Indeed Job Description Scraper](https://apify.com/thenetaji/indeed-job-description-scraper)
accepts. Both compose from this Actor's output with no lookup step in between.

`attributes` holds every taxonomy the card carries, keyed by the site's own names: benefits,
shift patterns and encouraged-to-apply tags all arrive there. Which keys exist varies per
posting, so a posting listing no benefits has no `benefits` key at all rather than an empty
one.

### Adding the full description

`enrichJobDescription` reads each saved job's own posting and merges it onto the row. It is
off by default, costs one further request per job, and is billed per job read, only after the
posting has been read.

It adds `description_html`, the posting as the employer wrote it, and `description_text`, the
same text with the markup removed and the line breaks kept — the form to hand to a keyword
filter or a language model. Alongside them come `valid_through`, `date_posted`,
`employment_type`, `direct_apply`, `requisition_id`, `postal_code`, `street_address`,
`locations`, `company_url` and `company_logo`.

`valid_through` is the useful one: it is the date the employer expects the posting to expire,
so a listing's freshness can be judged without fetching it again.

```json
{
  "queries": ["registered nurse"],
  "locations": ["Austin, TX"],
  "maxItems": 25,
  "enrichJobDescription": true
}
```

A job whose posting cannot be read keeps its search row unchanged and is not billed for the
add-on.

### Questions

**How many jobs does one search return?**
Roughly 40 to 50. Indeed serves exactly one page of results to a visitor without an account:
asking for the second page redirects to a sign-in wall, and Indeed's own "Next" link goes to
the same place. That ceiling belongs to Indeed rather than to this Actor, and no setting here
moves it. A measured search for `engineer` in Austin returned 48 jobs.

**So how is a market covered in full?**
By searching more, not deeper. Every term is run against every location, so the way to widen
a run is to add terms (`software engineer`, `backend developer`, `platform engineer`) and to
split a metro into its neighbouring cities, each of which gets its own page of results.
Changing `sort` from `relevance` to `date` also changes which jobs land on that single page,
so it is a coverage setting as much as an ordering one.

**Why did a second run of the same search return partly different jobs?**
Because Indeed rotates them. The same search twice, back to back, returned 42 jobs and 41
sharing only 31 — about a quarter of a page turns over between identical requests. Re-running
a saved input therefore does accumulate postings the first run never saw, which is unusual
and worth exploiting on a market being tracked over time.

**Why did the run save fewer rows than `maxItems`?**
Either the searches ran out first, or the same job turned up under more than one of them. A
job returned by two searches is saved once, and the row records the first search that found
it. The run log states how many jobs each search returned before deduplication.

**A search returned nothing at all — is that a failure?**
No. A search that matches nothing comes back as a normal, successful request with no results
on it, and the run logs a warning naming the term and continues to the remaining searches.
One dead term in a list of twenty does not end the run.

**Is `days_ago` a free-text number of days?**
No, and that is deliberate. Indeed acts on `1`, `3`, `7` and `14` and silently ignores
anything else, so accepting `10` would return an unfiltered result set that looked filtered.

**Is `radius` useful without a location?**
No. A nationwide search has no point to measure a radius from, so the value is ignored.

**Is an Indeed account, cookie or API key required?**
No. Nothing of the kind is supplied to the Actor or needed by it.

### Related Actors

[Indeed Job Description Scraper](https://apify.com/thenetaji/indeed-job-description-scraper)
reads full postings from the `job_key` column. It is the right choice for a second pass over
a dataset already collected, since it spends no requests finding the jobs again.

[Indeed Company Scraper](https://apify.com/thenetaji/indeed-company-scraper) profiles the
employers behind these rows from the `company_slug` column: ratings, CEO approval and the
work happiness survey.

[Indeed Salary Scraper](https://apify.com/thenetaji/indeed-salary-scraper) prices a job title
across the whole country when the question is what a role pays rather than who is hiring for
it.

# Actor input Schema

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

What to search for, one per line — a role, a skill, an employer name, or anything else the site's own search box accepts.

This is the main axis of coverage. The site serves exactly one page of results to anyone without an account, so a run collects more jobs by searching more terms rather than by paging deeper. Every term is searched against every location below, and a job found twice is saved once.

## `locations` (type: `array`):

Where to search, one per line: a city and state (`Austin, TX`), a state, a postcode, or `remote`. Leave the list empty for one nationwide search per term.

A run searches every term against every location, so four terms and five locations is twenty searches. Splitting one metro into its neighbouring cities is how a market is covered in depth, because each search is capped at a single page.

## `radius` (type: `integer`):

How far around each location to include. Meaningful only alongside a location — there is nothing to measure a radius from on a nationwide search. Left empty, the site applies its own default.

## `days_ago` (type: `string`):

Keep only postings first indexed inside this window. Restricted to the four windows the site acts on; any other value is accepted upstream and then ignored, which would quietly widen the search.

## `job_type` (type: `string`):

Keep only postings of one employment type. Left empty, every type is included.

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

Result ordering. Left empty, the site's own relevance ranking applies, which is what a visitor sees by default. Sorting by date changes which jobs land on the single available page, so it is a coverage setting as much as an ordering one.

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

Maximum number of rows to save across the whole run. Set 0 for no limit.

## `enrichJobDescription` (type: `boolean`):

Reads each saved job's own posting and attaches what a results card cannot carry: the complete description as the employer wrote it, both as HTML and as plain text, the expiry date the employer states, the employment type, the direct-apply flag, the requisition id, and the street-level address.

It costs one further request per job, so a run is materially slower with it on, and it is billed per job read.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "locations": [
    "Austin, TX"
  ],
  "radius": 25,
  "maxItems": 50,
  "enrichJobDescription": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

All records scraped by 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": [
        "software engineer"
    ],
    "locations": [
        "Austin, TX"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/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": ["software engineer"],
    "locations": ["Austin, TX"],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/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 '{
  "queries": [
    "software engineer"
  ],
  "locations": [
    "Austin, TX"
  ],
  "maxItems": 50
}' |
apify call thenetaji/indeed-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thenetaji/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/qW0szgm5xnG2j55x9/builds/19nrTcepVFFrJls3o/openapi.json
