# Indeed Job Listings Scraper with Salaries (`usestring/indeed-jobs`) Actor

Scrape Indeed job postings by search term and location across the US, UK, Canada, Australia and Germany. Every row carries the job title, company, location, the salary the employer stated in the posting, employer rating and review count, the posting date and a direct job URL.

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

## Pricing

from $1.50 / 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

This Actor collects job postings from Indeed search results. Give it a list of search terms,
optionally a location, and pick which Indeed domain to search. It returns one row per job with the
salary the employer wrote in the posting, the company's Indeed rating and a link to the posting
itself.

### What does the Indeed Job Scraper do?

It searches Indeed the way a logged-out visitor does, then reads the result set out of the page's
own data rather than out of the rendered cards. Indeed ships each search as a JSON payload assigned
to `window.mosaic.providerData["mosaic-provider-jobcards"]`, and everything on the results page is
drawn from that payload. Reading it directly is what keeps a search complete: when this Actor was
built, the model-driven extraction it replaced answered a search with 10 jobs where the page's own
blob carried 47, and filled `salary` from the estimated-pay promo sitting beside the results.

- Every job card on the results page reaches the dataset, not the subset a markup parse recovers.
- `salary` is the pay the employer stated, and `null` when the posting states none. Indeed's own
  salary estimate is never substituted for it.
- Five Indeed domains from one input field: `us`, `uk`, `ca`, `au`, `de`.
- `jobKey` is Indeed's own identifier and does not change between runs, so repeat runs de-duplicate
  on one column.
- No Indeed account, cookie or API key is involved at any point.

### What data does it extract?

| Field | Type | Description |
| --- | --- | --- |
| `jobKey` | string | Indeed's own job ID. Stable across runs, so use it as the de-duplication key |
| `title` | string | Job title as posted |
| `company` | string | Employer name |
| `location` | string | Where Indeed places the job, including the postcode when it shows one |
| `salary` | string | The posting's stated pay, e.g. `"$150,000 - $185,000 a year"`. `null` when the employer gave none |
| `companyRating` | number | The employer's Indeed rating out of 5 |
| `companyReviewCount` | number | How many Indeed reviews that rating is based on |
| `postedAt` | string | ISO 8601 timestamp of publication, converted from Indeed's own `pubDate` |
| `jobUrl` | string | Direct link to the posting, built from `jobKey` on the country's domain |
| `query` | string | The search term that produced the row |
| `country` | string | `us`, `uk`, `ca`, `au` or `de` |
| `searchLocation` | string | The location filter used, or `null` when the search was unfiltered |
| `sourceUrl` | string | The exact Indeed search URL the row came from |
| `collectedAt` | string | ISO 8601 timestamp of the run |

Missing values arrive as `null`. Nothing is inferred, guessed or filled in from a neighbouring
element on the page.

### Why scrape Indeed?

Compensation research is the common one. `salary`, `location` and `title` together give you
employer-stated pay for a role across cities, with none of Indeed's own estimate mixed in, which is
the difference between a benchmark you can defend and one you cannot.

The rest of what people do with it:

- Watch a competitor's roles weekly and see which teams are growing. `company` and `postedAt` date
  the shift.
- Fill a job board or an ATS with live postings, apply link already attached.
- Sort employers by how their own staff rate them, using `companyRating` and `companyReviewCount`,
  before anyone on your side makes an approach.
- Count postings per title per city over months to see where demand for a skill is moving.

### How to use it

1. Open the Actor on Apify Store and click **Try for free**.
2. Put your search terms in **Search queries**. One term per line, up to 50. `"software engineer"`,
   `"registered nurse"`, `"warehouse associate"`.
3. Set **Location** if you want the search narrowed, e.g. `"Austin, TX"` or `"London"`. Leave it
   empty for a nationwide search.
4. Set **Country** to the Indeed domain you want: `us`, `uk`, `ca`, `au` or `de`. One run searches
   one country.
5. Click **Start** and watch the log. Each search reports as it finishes.
6. Open the **Dataset** tab when the run ends and export to JSON, CSV, Excel or the API. Failed
   searches, if any, are listed in the `SUMMARY` record in the key-value store.

To keep the data current, save the input as a task and put it on an Apify schedule. Join runs on
`jobKey` and you get a clean history of when each posting appeared and disappeared.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `queries` | string\[] | required | Search terms, 1 to 50. Duplicates collapse to a single fetch |
| `location` | string | none | Location filter passed to Indeed. Omit for a nationwide search |
| `country` | string | `us` | Indeed domain: `us`, `uk`, `ca`, `au` or `de` |
| `maxItems` | integer | 1000 | Cap on dataset rows, 1 to 50000. Free plans stop earlier, see below |
| `concurrency` | integer | 3 | Searches fetched in parallel, 1 to 10 |

```json
{
  "queries": ["software engineer", "data analyst"],
  "location": "Austin, TX",
  "country": "us",
  "maxItems": 500,
  "concurrency": 3
}
```

### Output

One row per job. This is the shape, with values chosen to illustrate it rather than captured from a
particular run.

```json
{
  "jobKey": "0000aaaa1111bbbb",
  "title": "Senior Software Engineer",
  "company": "Northwind Logistics",
  "location": "Austin, TX 78701",
  "salary": "$150,000 - $185,000 a year",
  "companyRating": 3.7,
  "companyReviewCount": 1284,
  "postedAt": "2026-08-14T00:00:00.000Z",
  "jobUrl": "https://www.indeed.com/viewjob?jk=0000aaaa1111bbbb",
  "query": "software engineer",
  "country": "us",
  "searchLocation": "Austin, TX",
  "sourceUrl": "https://www.indeed.com/jobs?q=software+engineer&l=Austin%2C+TX",
  "collectedAt": "2026-08-20T09:31:02.118Z"
}
```

### Reliability

Measured across 30 distinct Indeed job searches in one run: 29 of 30 searches succeeded and 1,132
jobs were collected, with every schema field populated.

Because the rows come from Indeed's own payload, a field is either present and correct or `null`.
There is no model in the path to paraphrase a job title or round a rating.

A search that cannot be read is recorded in the run's `SUMMARY` under `failures` with the URL and
the error, and the rest of the run still returns its rows. A run in which every search failed exits
with an error rather than finishing quietly with an empty dataset.

### Limitations

One results page per search, so a run collects the first page for each term rather than paginating
a whole market. That is roughly 39 jobs per search. Widen coverage by running several narrower
queries, for example by title and city, instead of one broad one.

Full job descriptions, application counts and recruiter contact details are out of scope. Follow
`jobUrl` for the description. One run covers one country.

### How much does it cost?

Pay per event: you are charged for each result row written to the dataset, plus Apify's own platform
usage for the run. The current per-result price is on the Store page for this Actor, under Pricing.

A search that fails writes no rows and therefore charges nothing for that search. A run capped by
`maxItems` charges only for the rows it kept.

Runs started from an Apify **free plan** stop at 250 requests and 250 results, and the run reports
that it reached the limit in its status message. Any paid plan runs the full input and whatever
`maxItems` you set. The limit exists because this Actor fetches through our own anti-bot
infrastructure, which Apify does not cover for free-plan runs. It binds on requests as well as rows
so that a long input list cannot spend those fetches on rows the run will never return.

### Using it with the Apify API

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });

const run = await client.actor('usestring/indeed-jobs').call({
    queries: ['software engineer', 'data analyst'],
    location: 'Austin, TX',
    country: 'us',
    maxItems: 500
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const withPay = items.filter((job) => job.salary !== null);
console.log(`${items.length} jobs, ${withPay.length} with an employer-stated salary`);
```

The same call works from the Python client, and from any HTTP client through the Apify REST API.
The run's `SUMMARY` record, holding the item count and any failed searches, is in the run's default
key-value store.

### Is it legal to scrape Indeed?

This Actor reads public Indeed search pages as a logged-out visitor. It sends no account
credentials, no cookies and no API key, and it accesses nothing behind a login or a paywall. The
rows it returns are job advertisements that employers published for candidates to find, plus the
employer rating Indeed shows publicly next to them.

It returns no personal data: no candidate profiles, no recruiter names, no email addresses and no
phone numbers.

Scraping public data is broadly lawful in the US and the EU, but what you then do with it is
governed by your own jurisdiction, your contracts and, where personal data is involved, by GDPR and
similar regimes. This is not legal advice. If your use case is unusual, take your own.

### Related actors

- [Glassdoor Jobs Scraper - Pay and Ratings](https://apify.com/usestring/glassdoor-jobs)
- [ZipRecruiter Jobs Scraper](https://apify.com/usestring/ziprecruiter-jobs)
- [LinkedIn Public Profile Scraper (No Login)](https://apify.com/usestring/linkedin-profiles)
- [Wellfound Startup Jobs Scraper](https://apify.com/usestring/wellfound-jobs)
- [Greenhouse Job Board Scraper](https://apify.com/usestring/greenhouse-jobs)

Pair the Indeed scraper with Glassdoor for the same role and you can compare employer-stated pay
against Glassdoor's numeric ranges for the same companies.

### FAQ

**Do I need an Indeed account or API key?**
No. The Actor reads public Indeed search pages with no account, login, cookie or key.

**How many jobs does one search return, and can I paginate?**
Around 39, which is what a single Indeed results page carries, and there is no pagination input.
Ten search terms return roughly 390 jobs. To go deeper into a market, split the query: by seniority,
by city, or by a more specific title.

**Which countries are supported?**
Five, selected with `country`: the United States, United Kingdom, Canada, Australia and Germany.
Each maps to Indeed's own domain for that country. A run searches one of them, so use one run per
country.

**Is the salary real or Indeed's estimate?**
Real. `salary` is the string from the posting itself and is `null` when the employer stated no pay.
The estimated-salary panel Indeed renders beside the results is deliberately ignored, so an empty
`salary` means the employer was silent rather than that the Actor guessed wrong.

**How fresh are the results?**
Each run fetches live at the moment it starts. `postedAt` carries Indeed's publication date and
`collectedAt` the run timestamp, so you can measure how old a posting was when you saw it.

**What happens if Indeed blocks or a search fails?**
Requests go through our own anti-bot infrastructure, and a search that still cannot be read is
recorded under `failures` in the run's `SUMMARY` with its URL and error. Other searches in the run
are unaffected. If every search fails, the run ends in an error state so a schedule notices.

**Can I get the full job description?**
No. This Actor reads the search results page and does not open each posting. `jobUrl` takes you
there.

### Feedback

Something wrong in the data, or a field you need that is not here? Open an issue on the Actor's
Store page and we will read it. Bug reports that name the search term, country and date are the
fastest to act on.

# Actor input Schema

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

Job search terms, e.g. "software engineer".

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

Optional location filter.

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

Indeed domain: us, uk, ca, au or de.

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

Global cap on dataset items. Runs started from an Apify free plan stop at 250 requests and 250 results; any paid plan runs the full amount.

## `concurrency` (type: `integer`):

Targets fetched in parallel.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "country": "us",
  "maxItems": 1000,
  "concurrency": 3
}
```

# Actor output Schema

## `results` (type: `string`):

Scrape Indeed job postings by search term and location across the US, UK, Canada, Australia and Germany. Every row carries the job title, company, location, the salary the employer stated in the posting, employer rating and review count, the posting date and a direct job URL.

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

Item count, failure count and every target that failed, with its error.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("usestring/indeed-jobs").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"] }

# Run the Actor and wait for it to finish
run = client.actor("usestring/indeed-jobs").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"
  ]
}' |
apify call usestring/indeed-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,usestring/indeed-jobs"
        }
    }
}

```

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/ywZYtSaoq9Exxkifs/builds/co7f91o0oG3dVbmYG/openapi.json
