# Indeed Scraper (`dataharbor/indeed-scraper`) Actor

Scrape Indeed jobs with full descriptions in text and HTML — the blanks other scrapers leave empty are re-fetched and filled. Adds company profile (website, industry, size, CEO), verified apply links, and salaries. Search by keyword, Indeed URL, or a specific company.

- **URL**: https://apify.com/dataharbor/indeed-scraper.md
- **Developed by:** [Data Harbor](https://apify.com/dataharbor) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 6 total users, 4 monthly users, 84.5% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.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

## Indeed Scraper — Full Descriptions

Extract job listings from **Indeed** at scale: job title, full description (text **and** HTML),
salary, location, employment type, verified apply link, and company profile.
Search by keyword + location, paste Indeed **search URLs**, or scrape a specific **company's jobs** —
then download as JSON, CSV, Excel, or feed it straight into your own tools via the API.

Built around one thing most Indeed scrapers get wrong: **empty job descriptions.** Indeed's bulk
API returns a blank description for a large share of sponsored and partner-ATS listings. This actor
detects those and re-fetches the real description from the job page, so your dataset comes back with
the descriptions actually filled in — and ships a `descriptionEmpty` flag so you can verify it.

***

### What you can extract

- 🧑‍💼 **Job** — title, full description (plain text + HTML), employment type, remote flag, expired flag, posting date
- 💵 **Salary** — min, max, currency, and interval when Indeed publishes them
- 🔗 **Links** — the Indeed job URL and the **resolved employer apply URL** (the real link behind Indeed's redirect)
- 🏢 **Company** — website, description, industry, address, size, revenue, logo, and **CEO name**
- 📧 **Emails** — any contact emails found in the job description

> Company fields depend on Indeed publishing a company profile for that employer.
> See **[Field coverage — an honest breakdown](#field-coverage--an-honest-breakdown)** below for
> real numbers. Every job always includes title, description, location, posting date, and apply link.

***

### How this scraper compares

| | This actor | Typical Indeed scraper |
|---|---|---|
| Job descriptions | **Backfilled** — empties re-fetched from the job page | Often left blank for ~half of sponsored/ATS listings |
| Description format | Plain text **and** HTML | Sometimes one or neither |
| Verified apply link | ✅ resolves the real employer URL behind Indeed's redirect | Sometimes |
| Company profile | website, description, industry, size, revenue, logo, **CEO** | Only behind an extra step, if at all |
| Search by URL | ✅ search URLs **and** company-jobs URLs | Sometimes |
| Filter out staffing agencies | ✅ flag or drop agency-posted jobs (no LLM) | ✗ |
| Multiple keywords per run | ✅ one per line, run in parallel | Sometimes |
| Drop-in migration | ✅ optional field names matching other Indeed scrapers | — |
| Dedup | ✅ unique listings | Sometimes |
| Pricing | Pay per result — billed only for jobs returned | Varies |

***

### How to use it

1. Create a free Apify account.
2. Open this actor.
3. Enter one or more **job titles/keywords** and a **location** — or paste Indeed **Start URLs**.
4. Click **Start** and wait for the run to finish.
5. Download your data as **JSON, CSV, Excel, HTML, or XML**, or pull it via the [Apify API](https://docs.apify.com/api/v2).

***

### Input

You can drive the scraper three ways — by keyword, by Indeed search URL, or by company. Mix and match
in a single run; each search is capped independently by **Max job listings** and the results are merged
and de-duplicated.

| Field | Type | Default | Description |
|---|---|---|---|
| `position` | string | `accountant` | Job title or keywords. Put **one keyword per line** to run several searches at once. Optional if you provide `startUrls`. |
| `startUrls` | array | — | Indeed URLs to scrape: **search-result URLs** (e.g. `https://www.indeed.com/jobs?q=nurse&l=Austin`) and/or **company-jobs URLs** (e.g. `https://www.indeed.com/cmp/Google/jobs`). |
| `location` | string | — | City, state, or region to narrow keyword searches. |
| `country` | enum | `USA` | Indeed country domain (USA, UK, Canada, Australia, Ireland, India, Germany, France, Netherlands, Spain, Italy, Singapore, New Zealand, Mexico, Brazil). |
| `maxItems` | integer | `100` | Max listings **per search / per URL** (hard cap 5000). |
| `maxAgeDays` | integer | `7` | Only return jobs posted within this many days. |
| `scrapeCompanyDetails` | boolean | `true` | Add the company profile block (website, description, industry, address, size, revenue, logo, employer key, CEO). |
| `detectStaffingAgencies` | boolean | `false` | Add `isStaffingAgency` + `staffingAgencyReason` to each job — flags staffing/recruiting firms vs direct employers. |
| `excludeStaffingAgencies` | boolean | `false` | Drop agency-posted jobs entirely (direct employers only). You aren't billed for excluded rows. |
| `uniqueOnly` | boolean | `true` | Collapse duplicate listings (same job across searches). |
| `followApplyRedirect` | boolean | `true` | Resolve the real employer apply URL behind Indeed's redirect and return it as `applyUrl`. |
| `compatibleFieldNames` | boolean | `false` | Also emit alternate output keys that match the field names other Indeed scrapers commonly use, so you can switch to this actor without changing your code. See [Migrating](#migrating-from-another-indeed-scraper). |
| `proxyConfiguration` | object | Apify Proxy (datacenter) | Proxy used for scraping. **Required** — see [Proxy](#proxy-required). |

#### Example — keyword search

```json
{
  "position": "accountant",
  "location": "New York",
  "country": "USA",
  "maxItems": 200,
  "maxAgeDays": 7
}
```

#### Example — multiple keywords + a company's jobs

```json
{
  "position": "accountant\nbookkeeper\ncontroller",
  "startUrls": [
    { "url": "https://www.indeed.com/jobs?q=financial+analyst&l=Chicago" },
    { "url": "https://www.indeed.com/cmp/Deloitte/jobs" }
  ],
  "maxItems": 150,
  "maxAgeDays": 14
}
```

***

### Output

One record per job. Here is a **real, unedited** record (description truncated for space):

```json
{
  "title": "Budget Chief",
  "company": "State of Florida",
  "location": "Tallahassee, FL, US",
  "jobUrl": "https://www.indeed.com/viewjob?jk=42ba7f5dfd73a7a0",
  "applyUrl": "https://jobs.myflorida.com/job/TALLAHASSEE-Budget-Chief-FL-32399/1415668100/",
  "datePosted": "2026-08-04",
  "jobType": null,
  "isRemote": false,
  "isExpired": false,
  "salaryMin": null,
  "salaryMax": null,
  "currency": null,
  "interval": null,
  "description": "The Florida Legislature — Budget Committee … (full text)",
  "descriptionHTML": "<div> … full HTML … </div>",
  "descriptionEmpty": false,
  "emails": ["HouseAPD@FLHouse.gov"],
  "companyUrl": "https://www.indeed.com/cmp/State-of-Florida",
  "companyWebsite": "https://www.myflorida.com/",
  "companyIndustry": "Government",
  "companyDescription": "Seeking an opportunity to start a new career or enhance your professional development? …",
  "companyAddress": "Tallahassee, FL",
  "companyNumEmployees": "201 to 500",
  "companyRevenue": null,
  "companyLogo": "https://d2q79iu7y748jz.cloudfront.net/s/_squarelogo/256x256/15b41c73ae9b10833a18b747a07f3faf",
  "employerKey": "c8ebc9c7f7d88fc0",
  "ceoName": "Ron DeSantis"
}
```

#### Field reference

**Job fields** — present on every job:

| Field | Type | Notes |
|---|---|---|
| `title` | string | Job title |
| `company` | string | Employer name |
| `location` | string | Formatted location |
| `jobUrl` | string | Indeed job page |
| `applyUrl` | string | Resolved employer apply URL when `followApplyRedirect` is on; otherwise the Indeed URL |
| `datePosted` | string | `YYYY-MM-DD` |
| `isRemote` | boolean | Remote-work flag |
| `isExpired` | boolean | Whether Indeed marks the listing expired |
| `description` | string | Full description as text |
| `descriptionHTML` | string | Full description as HTML |
| `descriptionEmpty` | boolean | `false` means the description is present. Your built-in check that the backfill worked. |
| `jobType` | array | e.g. `["fulltime"]` — when Indeed tags it |
| `salaryMin` / `salaryMax` / `currency` / `interval` | number/string | When Indeed publishes a salary |
| `emails` | array | Emails found in the description, if any |

**Company fields** — added by `scrapeCompanyDetails`, populated when the employer has an Indeed profile:

| Field | Type | Notes |
|---|---|---|
| `companyUrl` | string | Indeed company page (`/cmp/…`) |
| `companyWebsite` | string | The employer's own website |
| `companyIndustry` | string | e.g. `Government`, `Accounting & Tax` |
| `companyDescription` | string | Short company blurb |
| `companyAddress` | string | Headquarters/primary address |
| `companyNumEmployees` | string | e.g. `201 to 500` |
| `companyRevenue` | string | Revenue band, when available |
| `companyLogo` | string | Logo image URL |
| `employerKey` | string | Indeed's internal employer id |
| `ceoName` | string | CEO name, when available |

**Staffing-agency fields** — added by `detectStaffingAgencies`:

| Field | Type | Notes |
|---|---|---|
| `isStaffingAgency` | boolean | Whether the posting looks like a staffing/recruiting agency rather than a direct employer |
| `staffingAgencyReason` | string | Which signals fired (company name, industry, description, or "our client" language) |

#### For recruiters & sales teams

A fresh job post is a **hiring-intent signal** — a company that needs to hire *right now*. This actor
gives you what you need to act on it: the **company website/domain** and **CEO name** to find and reach
the decision-maker, and a **staffing-agency filter** so you can screen out postings already handled by
recruiters and keep only direct employers. Turn on `excludeStaffingAgencies` for a clean list of
companies you can actually pitch or source for.

When a search finds nothing, the run finishes with a `FOUND_NO_RESULTS` status message and writes the
same signal to the run's key-value store — **without pushing a dataset item, so an empty run is never
billed.** Automations can still detect the empty result for free:

```json
{ "error": "FOUND_NO_RESULTS", "errorDescription": "Scraper didn't find any jobs for the given input" }
```

***

### Field coverage — an honest breakdown

Job fields (title, description, location, apply link, dates) are present on **essentially every listing**.
The company fields come from Indeed's employer profile, which **doesn't exist for every employer** —
sponsored and partner-ATS postings often have none. Typical coverage we observe:

| Field group | Typical coverage | Why it varies |
|---|---|---|
| Title, description, location, apply link | ~100% | Always available per listing |
| Company website / description / size / CEO / revenue | ~65–80% | From Indeed's job API — present when the employer has an Indeed profile |
| Company **industry** | ~10%+ | Indeed's job API returns it for only a minority of employers |

We don't fabricate missing values — if Indeed doesn't publish a field, it comes back `null`. Company
website, description, size, revenue, and CEO come straight from Indeed's job API, so they're the reliable
enrichment fields whenever the employer has an Indeed profile. Every job always has title, description,
location, and apply link.

***

### Proxy (required)

Indeed refuses connections from datacenter/hosting IPs, so runs need a proxy. This actor defaults to
**Apify Proxy (datacenter)**, which the job scrape and description backfill both use — cheap and fast.
Without any proxy, runs fail with a connection error.

Residential groups are selectable but rarely needed — they pull the same pages at many times the
bandwidth. Try a re-run on datacenter first; blocks are usually transient.

***

### Migrating from another Indeed scraper

Set `compatibleFieldNames: true` and the output also includes alternate keys that match the field
names other Indeed scrapers commonly use — `positionName`, `url`, `id`, `externalApplyLink`,
`postedAt`, `scrapedAt`, and `salary` — alongside this actor's own field names. That lets you point an
existing pipeline at this actor without rewriting your downstream code.

***

### Integrations & API

Runs and datasets are available through the whole Apify platform: schedule runs, trigger
[webhooks](https://docs.apify.com/platform/integrations/webhooks) on finish, and connect to **Make,
Zapier, Slack, Google Sheets, Google Drive, GitHub, Airbyte**, and more. Everything is also reachable
programmatically via the [Apify API](https://docs.apify.com/api/v2) and the `apify-client` packages
for [Python](https://docs.apify.com/api/client/python/) and
[JavaScript](https://docs.apify.com/api/client/js/).

***

### Pricing

**Pay per result** — you're billed only for the job listings the run returns. Company details are
included in each result at no extra per-field charge. The best way to estimate your cost is to run a
small test and multiply.

***

### Is scraping Indeed legal?

This actor collects only **publicly available** job-posting data — the same information any visitor
sees on Indeed. It does not access private accounts or bypass logins. Results can still contain
personal data (e.g. a contact email in a description); you are responsible for handling any personal
data lawfully and for your own use case. If you're unsure whether your use is permitted, consult a
lawyer. See Apify's guides on [ethical](https://blog.apify.com/what-is-ethical-web-scraping-and-how-do-you-do-it/)
and [legal](https://blog.apify.com/is-web-scraping-legal/) web scraping.

***

### FAQ

**Why are some company fields empty?** Because Indeed has no company profile for that employer.
Job fields (title, description, apply link) are always present; company fields fill in when a
profile exists — see [coverage](#field-coverage--an-honest-breakdown).

**Do I really get full descriptions?** That's the core feature: when Indeed's bulk response returns a
blank description, the actor re-fetches it from the job page. The `descriptionEmpty` flag on every
record lets you confirm it yourself — run the same query on any other scraper and compare.

**Can I scrape a specific company's jobs?** Yes — put its Indeed company-jobs URL
(`https://www.indeed.com/cmp/<Company>/jobs`) in `startUrls`.

**How many results can I get?** It varies by query, location, and country; Indeed itself limits how
deep any search goes. Set `maxItems` per search and run a test to see what your query returns.

**How fresh are the jobs?** Control it with `maxAgeDays` (default 7).

***

### Feedback

Found a bug or want a field added? Open an issue on the actor's **Issues** tab in Apify Console — the
scraper is actively maintained and tracks Indeed's changes.

# Actor input Schema

## `position` (type: `string`):

Job title or keywords to search on Indeed, e.g. "accountant". Enter multiple keywords on separate lines — each is scraped independently and capped by Max job listings. Optional if you provide Start URLs.

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

Indeed URLs to scrape. Supports search-result URLs (e.g. https://www.indeed.com/jobs?q=accountant\&l=New+York) and company-jobs URLs (e.g. https://www.indeed.com/cmp/Google/jobs). Each is scraped independently, capped by Max job listings. Runs alongside any keywords above.

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

Maximum number of job listings to return (capped at 5000).

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

Indeed country domain to search.

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

City, state, or region to narrow the search (optional).

## `maxAgeDays` (type: `integer`):

Only return jobs posted within this many days.

## `scrapeCompanyDetails` (type: `boolean`):

Include company industry, description, address, size, website, logo, and bonus employer key / CEO name.

## `detectStaffingAgencies` (type: `boolean`):

Add `isStaffingAgency` (true/false) and `staffingAgencyReason` to each job, flagging postings from staffing/recruiting firms vs direct employers. Uses company name, industry, description, and 'our client' language in the job — no LLM. Great for recruiters who only want direct employers. Most accurate with company details enabled.

## `excludeStaffingAgencies` (type: `boolean`):

Drop staffing/recruiting-agency postings from the results entirely, so you only get direct employers (and aren't billed for the agency rows). Implies staffing-agency detection.

## `uniqueOnly` (type: `boolean`):

Collapse duplicate listings (same company + title + location).

## `followApplyRedirect` (type: `boolean`):

Resolve the real employer apply URL behind Indeed's redirect and return it as applyUrl.

## `compatibleFieldNames` (type: `boolean`):

Also emit alternate output keys (positionName, url, id, externalApplyLink, postedAt, scrapedAt, salary) that match the field names other Indeed scrapers commonly use, so an existing pipeline can switch to this actor without changing code. This actor's own clean field names are always included too.

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

A proxy is required for the scrape to work on the Apify platform (Indeed refuses direct datacenter/hosting IPs). Apify Proxy (datacenter) is the tested default and is what this Actor is tuned for. Residential groups consume far more bandwidth and are rarely needed.

## Actor input object example

```json
{
  "position": "accountant",
  "maxItems": 50,
  "country": "USA",
  "maxAgeDays": 7,
  "scrapeCompanyDetails": true,
  "detectStaffingAgencies": false,
  "excludeStaffingAgencies": false,
  "uniqueOnly": true,
  "followApplyRedirect": true,
  "compatibleFieldNames": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All scraped Indeed job listings for this run (one item per job).

# 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 = {
    "position": "accountant",
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("dataharbor/indeed-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 = {
    "position": "accountant",
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("dataharbor/indeed-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 '{
  "position": "accountant",
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call dataharbor/indeed-scraper --silent --output-dataset

```

## MCP server setup

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