# Indeed Jobs Scraper — Detailed (`datasiphon/indeed-detailed-scraper`) Actor

Detailed Indeed job scraping: full descriptions, pre-parsed salaries, benefits, shifts, company ratings, normalized titles and resolved ATS apply URLs across 63 country domains. Fails loudly instead of returning an empty dataset.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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 — Detailed

Scrape Indeed job postings with the **full description**, **GPS coordinates**, pre-parsed
salary, employer **firmographics** (revenue, headcount, CEO), skills and the employer's real
**ATS apply URL** — across **63 Indeed country domains**, with deep cursor pagination.

HTTP-only, no headless browser. A 200-job run costs **2 requests**, because every job arrives
complete — descriptions are not fetched one page at a time.

Two things no other Indeed scraper on the store does: **incremental runs** that return only
what is new since last time, and **a run that fails loudly instead of handing you an empty
dataset** when it was blocked.

### What you get per job

```json
{
  "jobKey": "9e9e0856fe8e38c7",
  "url": "https://www.indeed.com/viewjob?jk=9e9e0856fe8e38c7",
  "title": "Software Development Engineering - Advisor I",
  "normalizedTitle": null,
  "company": "Fiserv",
  "companyUrl": "https://www.indeed.com/cmp/Fiserv",
  "companyRating": null,
  "companyReviewCount": null,
  "location": "Berkeley Heights, NJ 07922",
  "city": "Berkeley Heights",
  "state": null,
  "country": "US",
  "isRemote": false,
  "salaryText": "$110,000 - $186,000 a year",
  "salaryMin": 110000,
  "salaryMax": 186000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEARLY",
  "salaryIsEstimate": false,
  "jobTypes": [
    "Full-time"
  ],
  "shifts": [],
  "schedules": [],
  "benefits": [],
  "postedAt": "2026-08-24T05:00:00.000Z",
  "postedRelative": null,
  "scrapedAt": "2026-08-24T23:06:15.390Z",
  "isSponsored": false,
  "isNew": false,
  "isExpired": false,
  "isUrgentlyHiring": false,
  "isEasyApply": false,
  "applyUrl": "https://careers.fiserv.com/us/en/job/R-10401161...",
  "atsUrl": "https://careers.fiserv.com/us/en/job/R-10401161",
  "atsProvider": "careers.fiserv.com",
  "descriptionHtml": "<div></div><div><div><div><div>Location: Berkeley Heights,...",
  "descriptionText": "Location: Berkeley Heights, New Jersey, United States of America Job ID: R-10401161 Category...",
  "descriptionSnippet": null,
  "occupationIds": [
    "5NN53",
    "EHPW9",
    "HJSX6"
  ],
  "occupations": [
    "Software Development Occupations",
    "Technology Occupations",
    "Software Development & Architecture Occupations"
  ],
  "attributes": [
    "Software engineering",
    "Azure",
    "Continuous Delivery (CD) implementation",
    "Engineering development testing",
    "Requirements design",
    "C#"
  ],
  "latitude": 40.665768,
  "longitude": -74.41226,
  "streetAddress": "100 Connell Drive",
  "postalCode": "07922",
  "language": "en",
  "isUrgentHire": false,
  "isHighVolumeHiring": false,
  "companyIndustry": null,
  "companySize": "10,000+",
  "companyRevenue": "more than $10B (USD)",
  "companyCeo": "Mike Lyons",
  "companyAddresses": [
    "Milwaukee, WI"
  ],
  "source": "graphql",
  "hiringInsights": {
    "isUrgentHire": false,
    "isHighVolumeHiring": false
  },
  "feedId": null,
  "sourceId": null,
  "searchQuery": "software engineer",
  "searchLocation": "New York, NY"
}
```

That is a real, unedited row (long URLs and description text trimmed for display).

**Fill rates, measured on a 200-job GraphQL run** — what to expect, not what to hope for:

| Field | Filled |
|---|---|
| `descriptionText`, `latitude`, `longitude`, `attributes`, `isExpired`, `language` | **200/200** |
| `occupations` | 199/200 |
| `atsProvider` | 184/200 |
| `salaryText` | 181/200 |
| `companySize` | 156/200 |
| `companyRevenue` | 142/200 |
| `companyCeo` | 111/200 |
| `postalCode` | 99/200 |
| `streetAddress` | 72/200 |
| `companyIndustry` | 32/200 |

All 200 rows had distinct `jobKey`s. Firmographic gaps mean Indeed holds no company profile for
that employer — see **Honest limits**.

### Input

#### Search by keyword and location

```json
{
  "queries": ["software engineer"],
  "locations": ["New York, NY"],
  "country": "US",
  "maxItems": 200
}
```

#### Indeed search operators

The `queries` field takes Indeed's own search syntax:

| Syntax | Matches |
|---|---|
| `data engineer` | the words anywhere in the posting, plus related roles |
| `"data engineer"` | that exact phrase anywhere in the posting |
| `title:(data engineer)` | both words in the job title, any order |
| `title:("data engineer")` | the exact phrase in the job title (strictest) |
| `company:Google` | every job from one employer |
| `nurse -travel` | excludes postings containing "travel" |

Combine with `and` / `or` / `not` and parentheses:
`title:(designer) -intern (remote or hybrid)`.

#### Lead generation: find companies hiring, and what they hire with

```json
{
  "queries": ["title:(\"account executive\")"],
  "locations": ["Austin, TX"],
  "maxItems": 500
}
```

Every row names the ATS behind the employer's apply link — Greenhouse, Lever, Workday, Ashby,
iCIMS, SmartRecruiters, Workable, BambooHR and more — at no extra request, alongside company
revenue, headcount and CEO. Use the **Hiring companies** dataset view for a company-level table.

(On the HTML engine the same field costs one extra request per job and must be enabled with
`resolveApplyUrls`. On the default engine it is always there.)

### Two engines

**GraphQL (default).** Indeed's mobile API. One request returns up to 100 jobs *with* full
descriptions, GPS, employer firmographics and the resolved apply URL, and pages by cursor with
no sign-in wall. A 200-job run costs **2 requests**.

**HTML (fallback).** Scrapes search pages. Used automatically if the API fails, and selectable
with `engine: "html"`. Logged-out Indeed serves it one page per search (~15-48 jobs), so it
reaches volume through query fan-out rather than depth, and full descriptions cost one extra
request per job.

The HTML engine is **best-effort, not a guarantee**: Indeed blocks its search pages far more
aggressively than the API, and on a bad exit IP it returns 403 outright. When that happens the
run fails with the reason instead of handing you an empty dataset. Treat GraphQL as the engine
and HTML as insurance, not as an equal second path.

The engines do **not** return identical fields, and no row pretends otherwise — every job
carries a `source` field naming the engine that produced it:

| Field | GraphQL | HTML |
|---|---|---|
| `descriptionText` | yes | yes (1 extra request/job) |
| `latitude` / `longitude` / `streetAddress` / `postalCode` | **yes** | no |
| `companyIndustry` / `companySize` / `companyRevenue` / `companyCeo` | **yes** | no |
| `attributes` (skills, licences, seniority) | **yes** | partial |
| `occupations` (readable labels) | **yes** | codes only |
| `atsUrl` / `atsProvider` | yes, free | yes, +1 request (`resolveApplyUrls`) |
| `isExpired` | **yes, real** | flag only |
| `companyRating` / `companyReviewCount` | no | **yes** |
| `benefits` / `isSponsored` / `normalizedTitle` | no | **yes** |

If you need company ratings, run `engine: "html"`. For everything else the default is better
and far cheaper.

### Honest limits — read this before you buy

- **Sponsored placement.** Much of Indeed's first HTML page is paid placement. The HTML engine
  flags every job with `isSponsored`; set `includeSponsored: false` to drop them. GraphQL does
  not expose that flag, so on the default engine `isSponsored` is always `false`.
- **Firmographics are not universal.** Indeed only holds them for employers with a company
  profile. Measured on a 200-job run: `companySize` 156/200, `companyRevenue` 142/200,
  `companyCeo` 111/200, `companyIndustry` 32/200. Absent means Indeed has no data, not an error.
- **`streetAddress` is partial** (72/200) — coordinates are always present, street level is not.
- **Residential proxies are strongly recommended.** Datacenter IPs get security-checked on the
  HTML routes.
- **This Actor reads a session key from Indeed's own web client.** If Indeed changes that, the
  run falls back to the HTML engine and says so in the log rather than dying.

### Reliability

In that run the 7 jobs whose description page was blocked were still emitted with their
listing data and `descriptionText: null` — 39 fetched + 1 free pane + 7 degraded = 47 rows,
nothing dropped.

A run that scrapes **zero jobs fails loudly** with the reason — every page blocked, or no
matching jobs — instead of finishing green with an empty dataset. If a job's description page
is blocked, the job is still emitted with its listing-level data and `descriptionText: null`,
so a bad IP costs you a field rather than the row.

Every run writes a `CAPABILITY_REPORT` record to the key-value store: which engine ran, how many
GraphQL pages and jobs it returned, whether it fell back to HTML, whether the API key was
harvested or a fallback was used, and what was blocked. If a run disappoints you, that record
says why.

### Cost control

On the default GraphQL engine there is nothing to tune: descriptions, GPS, firmographics and
the ATS URL all arrive in the same request, 100 jobs at a time. `maxItems` is the only lever.

These inputs apply to the **HTML engine only**, and the run warns you in the log if you set
them while GraphQL is active:

- `scrapeFullDescription` — off gives a listings-only crawl, one request per search page
  instead of one per job.
- `resolveApplyUrls` — one extra request per job to resolve the ATS link.
- `fanOut`, `maxPagesPerSearch`, `sort`, `jobTypes`, `experienceLevels`, `includeSponsored` —
  ways of working around the HTML engine's one-page-per-search wall, which GraphQL does not have.

### Is scraping Indeed legal?

Scraping publicly available job listings is generally legal, and this Actor collects only
public, non-personal data — job postings and employer information, no candidate profiles or
resumes. You are responsible for how you use the output, including under GDPR/CCPA where
relevant. When in doubt, take legal advice.

# Actor input Schema

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

Job searches to run. Supports Indeed's operators: "data engineer" (exact phrase), title:("data engineer") (title only), company:Google (one employer), nurse -travel (exclude). Combine with and/or/not.

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

City, state, ZIP or region. Each location is searched against every query. Leave empty to search the whole country.

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

Which Indeed domain to search. 63 supported.

## `engine` (type: `string`):

GraphQL uses Indeed's mobile API: one request returns up to 100 fully-detailed jobs with GPS and employer firmographics, and it paginates deep. HTML scrapes search pages and is the fallback. Auto tries GraphQL and falls back on failure.

## `incremental` (type: `boolean`):

Remembers job keys between runs and emits only new postings (plus any that have since expired). Use it to monitor a search instead of re-downloading it.

## `stateKey` (type: `string`):

Names the memory used by incremental mode. Runs sharing a key share history. Defaults to the query + location + country.

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

Paste Indeed search URLs directly instead of (or as well as) building them from queries.

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

Stop after this many jobs.

## `scrapeFullDescription` (type: `boolean`):

\[HTML engine only] Fetch the complete posting text for every job. Costs one extra request per job — turn off for a fast, cheap listing-level crawl.

## `fanOut` (type: `boolean`):

\[HTML engine only] Logged-out Indeed serves one page of results per search. Fan-out re-runs each query across both sort orders and several date windows, then de-duplicates, to reach far more jobs. Costs more requests.

## `resolveApplyUrls` (type: `boolean`):

\[HTML engine only] Follow Indeed's apply redirect to the real destination and name the ATS (Greenhouse, Lever, Workday, Ashby, iCIMS...). Costs one extra request per job and needs a residential proxy.

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

\[HTML engine only] Roughly two thirds of Indeed's first page is paid placement. Every job is flagged with isSponsored either way.

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

\[HTML engine only] Relevance is Indeed's default ranking. Date posted surfaces the newest listings first — and returns a different result set, which is why fan-out uses both.

## `maxDaysOld` (type: `integer`):

Only jobs posted in the last N days. Leave empty for no limit.

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

Distance from the location, in the country's default unit. Ignored when no location is set.

## `jobTypes` (type: `array`):

\[HTML engine only] Restrict to these employment types. Leave empty for all.

## `experienceLevels` (type: `array`):

\[HTML engine only] Restrict to these seniority levels. Leave empty for all.

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

Only jobs Indeed tags as remote.

## `maxPagesPerSearch` (type: `integer`):

\[HTML engine only] Indeed shows a sign-in wall past page 1 for anonymous traffic. Raising this makes the Actor attempt deeper pages and report in the log whether they worked; it never fails the run if they are walled.

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

Parallel requests. Lower this if you see blocks.

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

Residential proxies are strongly recommended: they are what unlocks deep pagination and apply-URL resolution.

## `dedupeByTitleCompany` (type: `boolean`):

Indeed lists the same opening from multiple feeds under different job keys. Turn this on to keep only the first of each title+company pair. Off by default because a company hiring the same role in several cities would also be collapsed.

## Actor input object example

```json
{
  "queries": [
    "title:(\"data engineer\")",
    "company:Google"
  ],
  "locations": [
    "New York, NY"
  ],
  "country": "US",
  "engine": "auto",
  "incremental": false,
  "maxItems": 200,
  "scrapeFullDescription": true,
  "fanOut": false,
  "resolveApplyUrls": false,
  "includeSponsored": true,
  "sort": "relevance",
  "radius": 25,
  "remoteOnly": false,
  "maxPagesPerSearch": 1,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "dedupeByTitleCompany": false
}
```

# Actor output Schema

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

No description

## `capabilityReport` (type: `string`):

No description

# 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": [
        "New York, NY"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datasiphon/indeed-detailed-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": ["New York, NY"],
}

# Run the Actor and wait for it to finish
run = client.actor("datasiphon/indeed-detailed-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": [
    "New York, NY"
  ]
}' |
apify call datasiphon/indeed-detailed-scraper --silent --output-dataset

```

## MCP server setup

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