Indeed Job Listings Scraper with Salaries avatar

Indeed Job Listings Scraper with Salaries

Pricing

from $1.50 / 1,000 results

Go to Apify Store
Indeed Job Listings Scraper with Salaries

Indeed Job Listings Scraper with Salaries

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.

Pricing

from $1.50 / 1,000 results

Rating

0.0

(0)

Developer

String

String

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

11 days ago

Last modified

Share

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?

FieldTypeDescription
jobKeystringIndeed's own job ID. Stable across runs, so use it as the de-duplication key
titlestringJob title as posted
companystringEmployer name
locationstringWhere Indeed places the job, including the postcode when it shows one
salarystringThe posting's stated pay, e.g. "$150,000 - $185,000 a year". null when the employer gave none
companyRatingnumberThe employer's Indeed rating out of 5
companyReviewCountnumberHow many Indeed reviews that rating is based on
postedAtstringISO 8601 timestamp of publication, converted from Indeed's own pubDate
jobUrlstringDirect link to the posting, built from jobKey on the country's domain
querystringThe search term that produced the row
countrystringus, uk, ca, au or de
searchLocationstringThe location filter used, or null when the search was unfiltered
sourceUrlstringThe exact Indeed search URL the row came from
collectedAtstringISO 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

FieldTypeDefaultDescription
queriesstring[]requiredSearch terms, 1 to 50. Duplicates collapse to a single fetch
locationstringnoneLocation filter passed to Indeed. Omit for a nationwide search
countrystringusIndeed domain: us, uk, ca, au or de
maxItemsinteger1000Cap on dataset rows, 1 to 50000. Free plans stop earlier, see below
concurrencyinteger3Searches fetched in parallel, 1 to 10
{
"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.

{
"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

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.

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.

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.