# Indeed Jobs — Stops at Your Limit (`steadyfetch/indeed-jobs-scraper`) Actor

Never pay for a job we did not deliver. Set a row cap and a deadline and the run stops exactly there — no runaway bills. Exact-country search: UK means UK. Title, company, location, salary, posted date and apply link on every row. Type a keyword or paste an Indeed search URL.

- **URL**: https://apify.com/steadyfetch/indeed-jobs-scraper.md
- **Developed by:** [Steadyfetch Team](https://apify.com/steadyfetch) (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 $1.50 / 1,000 job listings

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 — Stops at Your Limit

**Never pay for a job we didn't deliver.** Set a row cap and a deadline and the run stops
exactly there — no runaway bills. Exact-country search: UK means UK.

**From $1.50/1,000 job listings** — all-inclusive pay per event, no start fee, charged only
on delivery.

> **Unofficial.** This actor is not affiliated with, endorsed by, or sponsored by Indeed.
> "Indeed" and all related marks are trademarks of Indeed, Inc. It reads only the public
> search pages an ordinary browser sees.

Every row carries `charged` and `missReason`, so you can reconcile the invoice from the
dataset itself without opening the console. **Only rows with `charged: true` were billed.**

***

### Output — a real row

```json
{
  "source": "indeed",
  "sourceJobId": "c1e2f8002d59802e",
  "jobUrl": "https://uk.indeed.com/viewjob?jk=c1e2f8002d59802e",
  "title": "Software Engineer III - AI/ML Platform Reliability",
  "company": "JPMorganChase",
  "location": { "raw": "Glasgow G2", "city": "Glasgow G2", "region": null, "country": null },
  "isRemote": false,
  "employmentType": "Full-time",
  "salary": null,
  "salaryText": null,
  "postedAt": "2026-08-11T05:00:00.000Z",
  "postedRelative": "15 days ago",
  "snippet": "Formal training or certification on software engineering concepts and proficient applied experience…",
  "companyRating": 3.9,
  "companyReviewCount": 20415,
  "sponsored": false,
  "query": "software engineer",
  "country": "GB",
  "dedupeKey": "indeed:c1e2f8002d59802e",
  "charged": true,
  "missReason": null
}
```

**[Browse the full live example dataset →](https://api.apify.com/v2/datasets/PVUFmSMmyJqy1RMmy/items?clean=true\&format=json)** — one real run, unedited: five delivered US listings capped at `maxItems: 5`, the search row that reconciles them, and the run's own summary row last. Five listings delivered, five charged.

Plus **one search row per search term and country** — `query`, `country`, `delivered`,
`outcome` — and **one summary row** with `delivered`, `stoppedBy`, `resumeCursor` and
`chargedEvents`.

`jobUrl` is the canonical Indeed link built from the job's own key: identical across runs,
with no ad or tracking token in it, so it is safe to use as a join key.

### Agent / API paste-block

```
Actor:      steadyfetch/indeed-jobs-scraper
Required:   queries        (array of job titles/keywords, or full Indeed search URLs)
Optional:   countries          (array of ISO-2, default ["US"] — each is its own search)
            location           (string, e.g. "Austin, TX")
            radiusKm           (integer, needs a location)
            postedWithinDays   (integer, 1-30)
            sortBy             (relevance | date)
            remoteOnly         (boolean, default false — filtered rows are never charged)
            includeDescription (boolean, default false — fetch every job's full description)
            maxItems           (integer, default 100 — hard cap for the whole run)
            maxRunSeconds      (integer, default 900 — clean stop, never a timeout kill)
Charges:    job-listing      once per delivered job listing
            job-description  once per DELIVERED full description (only with
                             includeDescription: true; misses are never charged)
Note:       omit a field to use its default — do NOT send null.
Build spec: https://apify.com/steadyfetch/indeed-jobs-scraper/api
Token:      https://console.apify.com/settings/integrations
```

```bash
curl -X POST "https://api.apify.com/v2/acts/steadyfetch~indeed-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"queries":["software engineer","data analyst"],"countries":["GB"],"maxItems":30}'
```

***

### How many jobs one search returns

**About 15.** Indeed's results page holds roughly 15 listings and its "next page" no longer
returns results — asking for page two lands on a sign-in wall. So a bigger `maxItems` does
not produce more rows on its own: **more rows come from more search terms and more
countries.** Two terms across three countries is six searches, or roughly 90 listings.

We say this plainly because the alternative is you paying for a run that quietly returns 15
rows when you asked for 500. The summary row repeats it every run.

### Descriptions: what is free, and what the option adds

Every search ships two things at no extra cost:

- a **short plain-text summary for every listing** (`snippet`, measured 121–163 characters), and
- the **complete description for exactly one listing per search** — the job Indeed pre-opens
  in its side panel, around 6,000 characters, marked `descriptionSource: "search-page"`.

**"Fetch the full description for every job"** (`includeDescription`, off by default) is
**available now**: it fetches each remaining listing's complete description from Indeed's own
job panel, marked `descriptionSource: "job-panel"`. The honesty rules are the same as
everywhere else here:

- a description is charged **only when it actually comes back** — every row carries
  `chargedDescription`, so the invoice reconciles from the dataset;
- a description we could not fetch is reported on its own row, **uncharged**, and the listing
  itself is unaffected;
- we verify each description belongs to the exact job on the row before delivering it —
  a wrong or blocked answer becomes an uncharged miss, never someone else's text;
- if the option is ever not active on a run, that run tells you once in its log and
  charges nothing extra.

The per-description price is on the Pricing tab, as the **Full description** charge.

### Countries

`countries` picks Indeed's own country sites. **United States, United Kingdom and Germany
are verified end to end.** Other countries use the same mechanism, and every listing's
country is checked against what you asked for: anything that comes back outside it is
reported as an uncharged `GEO_MISMATCH` row instead of being sold to you as a match.

### What you are never charged for

| situation | `missReason` | run status |
|---|---|---|
| Indeed answered with an access check | `CHALLENGED` | SUCCEEDED — re-runnable, and the row says so |
| Indeed rate-limited us | `THROTTLED` | SUCCEEDED — re-runnable |
| Indeed returned a page we could not read | `SOURCE_UNAVAILABLE` | SUCCEEDED — re-runnable |
| The search ran and matched nothing | `CLEAN_EMPTY` | SUCCEEDED — a real answer |
| A listing outside the country you asked for | `GEO_MISMATCH` | SUCCEEDED — reported, not sold |
| A search term or URL we could not read | `USER_INPUT` | SUCCEEDED — the row names the fix |
| A full description that could not be fetched | *(reported on its job row)* | SUCCEEDED — the listing still ships; no Full description charge |
| Your own `maxItems` / `maxRunSeconds` / cost cap | `STOPPED_AT_LIMIT` | SUCCEEDED — summary row + `resumeCursor` |

We never guess. An access check is reported as an access check, not as "no jobs found" — a
temporary problem is never dressed up as a permanent verdict, and it is never charged.

**This actor may fail when Indeed changes things.** The two you are most likely to see are
`CHALLENGED` (Indeed answered with an access check) and `THROTTLED` (Indeed rate-limited
us). Both ship as a row with **`charged: false`**, the run still SUCCEEDS, and the row says
re-running can help.

### Limits are hard limits

`maxItems` and `maxRunSeconds` stop the run exactly where you set them. The run still
finishes **successfully**, the summary row names which limit bound, and `resumeCursor` lists
the searches that were not reached — so you continue from there instead of paying to re-walk
what you already have.

They are limits on the **run**, not on a container. If Apify moves your run to another server
part-way through, or you resurrect a finished run, it picks up from what is already in your
dataset: the same job is never delivered twice and never charged twice, and `maxItems` still
means what you typed.

### FAQ

**Why did I get 15 rows when I asked for 100?**
Because you gave one search term in one country. See "How many jobs one search returns" —
add terms or countries, not a bigger cap.

**Can I paste an Indeed search URL?**
Yes. A pasted search keeps its own country, location and filters, and is not crossed with
your `countries` list. A link to a single job posting is refused with a guidance row.

**Why is `salary` empty on so many rows?**
Because Indeed did not show one. `salaryText` is the exact string Indeed printed, and
`salary` is our parse of it; when there is no salary on the listing both stay `null` rather
than being estimated.

**Does `{}` charge anything?**
No job listings are charged. With no search terms the run returns uncharged sample rows that
show the exact output shape, and it contacts Indeed not at all.

***

### Steadyfetch jobs suite

One actor per source, one job each, the same row shape — so a pipeline built on one source takes the
next without a rewrite. All-inclusive pay per event, no start fee, charged only on delivery.

| Source | Actor |
|---|---|
| Indeed | **this actor** |
| A company's own career site (paste a domain) | **[Career-Site Jobs — Paste a Domain](https://apify.com/steadyfetch/company-jobs-by-domain)** |
| Glassdoor, with the employer rating | **[Glassdoor Jobs + Employer Rating](https://apify.com/steadyfetch/glassdoor-jobs-scraper)** |
| Several boards at once, de-duplicated | **[Multi-Board Jobs — No Duplicates](https://apify.com/steadyfetch/multi-job-board-scraper)** |

**The rest of the steadyfetch shelf** — same contract everywhere: all-inclusive pay per event,
no start fee, charged only on delivery.

| Family | Actors |
|---|---|
| Ad creative intelligence | [Facebook](https://apify.com/steadyfetch/facebook-ads-transcript-scraper) · [Google Ads video](https://apify.com/steadyfetch/google-ads-video-transcript-scraper) · [TikTok](https://apify.com/steadyfetch/tiktok-ads-transcript-scraper) · [LinkedIn](https://apify.com/steadyfetch/linkedin-ads-transcript-scraper) · [Google Ads text & OCR](https://apify.com/steadyfetch/google-ads-creative-text-scraper) |
| Trends & keywords | [Google Trends](https://apify.com/steadyfetch/google-trends-scraper) · [Trends Now](https://apify.com/steadyfetch/google-trends-now-scraper) · [Breakout keywords](https://apify.com/steadyfetch/breakout-keywords-scraper) · Autocomplete keywords · Keyword volume & CPC · [Social trends](https://apify.com/steadyfetch/social-trends-scraper) |
| YouTube transcripts | [YouTube videos](https://apify.com/steadyfetch/youtube-transcript-scraper) · [YouTube channels](https://apify.com/steadyfetch/youtube-channel-transcripts) |
| Instagram | [Reel transcripts](https://apify.com/steadyfetch/instagram-reel-transcript-scraper) · [Profile posts](https://apify.com/steadyfetch/instagram-profile-posts) |
| **Jobs** | [Indeed](https://apify.com/steadyfetch/indeed-jobs-scraper) · [Career sites by domain](https://apify.com/steadyfetch/company-jobs-by-domain) · [Glassdoor](https://apify.com/steadyfetch/glassdoor-jobs-scraper) · [Multi-board](https://apify.com/steadyfetch/multi-job-board-scraper) |
| Amazon | [Products](https://apify.com/steadyfetch/amazon-product-scraper) · [Search](https://apify.com/steadyfetch/amazon-search-scraper) · Bestsellers · Sellers |
| Any media file | Speech to Text · any link or file |

Unlinked names are publishing shortly on the same account — search **steadyfetch** on Apify Store.

# Actor input Schema

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

Job titles or keywords (for example "software engineer"). You can also paste a full Indeed search URL such as https://uk.indeed.com/jobs?q=data+analyst — a pasted search keeps its own country and filters. Leave the default in place to see uncharged sample rows first.

## `countries` (type: `array`):

Which Indeed country sites to search. Each country is a separate search, so two terms across two countries is four searches. United States, United Kingdom and Germany are verified end to end; other countries use Indeed's own country site and any listing that comes back outside the country you asked for is reported and never charged.

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

City, region or postcode to search near, as you would type it on Indeed (for example "Austin, TX"). Leave empty to search the whole country.

## `radiusKm` (type: `integer`):

How far around the location to search. Only used when a location is set.

## `postedWithinDays` (type: `integer`):

Only jobs Indeed posted in the last N days.

## `sortBy` (type: `string`):

Ask Indeed to order results by date instead of relevance.

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

Keep only listings Indeed marks as remote. Listings that are filtered out are never charged.

## `includeDescription` (type: `boolean`):

Off by default. When on, each delivered full description costs one extra Full description charge — and only when a description actually comes back; a description that cannot be fetched is reported on its row and never charged. If this option is not yet active on this listing, the run says so and charges nothing extra.

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

Hard cap on delivered job listings across every search in this run.

## `maxRunSeconds` (type: `integer`):

The run stops cleanly before this many seconds and reports what is left, instead of being killed by a timeout.

## Actor input object example

```json
{
  "queries": [
    "software engineer",
    "data analyst"
  ],
  "countries": [
    "US"
  ],
  "sortBy": "relevance",
  "remoteOnly": false,
  "includeDescription": false,
  "maxItems": 100,
  "maxRunSeconds": 900
}
```

# Actor output Schema

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

One row per delivered job listing: title, company, location, salary, posted date, apply link and Indeed's own search summary. Every row carries `charged` and `missReason`, so the invoice reconciles from the dataset itself. Only rows with charged = true were billed.

## `searches` (type: `string`):

One row per search term and country: how many jobs it delivered, or why it delivered none. Searches that were blocked, empty or filtered out are never charged.

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

Delivered count, searches run, uncharged misses, what stopped the run, and the charged-event totals.

## `errors` (type: `string`):

Present only when Indeed answered a search with an access check, a rate limit or an unreadable page: the search and the reason. These are re-runnable, not permanent.

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

// Run the Actor and wait for it to finish
const run = await client.actor("steadyfetch/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",
        "data analyst",
    ] }

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

```

## MCP server setup

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