Indeed Job Listings Scraper with Salaries
Pricing
from $1.50 / 1,000 results
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
11 days ago
Last modified
Categories
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.
salaryis the pay the employer stated, andnullwhen 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. jobKeyis 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.
companyandpostedAtdate 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
companyRatingandcompanyReviewCount, 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
- Open the Actor on Apify Store and click Try for free.
- Put your search terms in Search queries. One term per line, up to 50.
"software engineer","registered nurse","warehouse associate". - Set Location if you want the search narrowed, e.g.
"Austin, TX"or"London". Leave it empty for a nationwide search. - Set Country to the Indeed domain you want:
us,uk,ca,auorde. One run searches one country. - Click Start and watch the log. Each search reports as it finishes.
- 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
SUMMARYrecord 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 |
{"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.
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
- ZipRecruiter Jobs Scraper
- LinkedIn Public Profile Scraper (No Login)
- Wellfound Startup Jobs Scraper
- Greenhouse Job Board Scraper
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.

