People search for an "Indeed API" and find a dead end, then assume they're missing something. They're not. Indeed shut down its open Publisher Job Search API, and the APIs it still runs are scoped to employers, applicant-tracking systems and sponsored-job posting — none of which let you pull arbitrary listings across the market. If you want a dataset of who's hiring for what, where, and at what pay, the public search results page is the only door. This guide is less about how to fetch the pages and more about the three things that turn raw scraped HTML into data you can trust.
Why there's no clean feed to begin with
Indeed is an aggregator. A huge share of its listings are syndicated in from company career sites, ATS platforms and staffing agencies, then re-surfaced by relevance and recency. That architecture is exactly why an open listings API would be a firehose Indeed has no incentive to ship — and it's also the root cause of the duplication problem you'll hit five minutes into any serious crawl.
The fields you can actually pull
A scraped listing gives you a predictable record. The ones that matter:
Problem 1 — reposts will inflate your counts by 30%+
The same opening shows up again and again: posted by the company, re-posted by two staffing agencies, then re-surfaced a week later with a fresh timestamp. If you count rows, your "open roles" number is fiction. The fix is a composite dedup key, not the URL or jobKey:
dedup_key = normalize(company) + "|" + normalize(title) + "|" + normalize(location)
# normalize(): lowercase, strip punctuation, collapse whitespace,
# drop common suffixes ("inc", "llc", "ltd"), map "remote" variants to one token
Keep the earliest datePosted among collisions as the true post date, and keep the longest description (agency reposts are often truncated). Now your counts mean something.
Problem 2 — salary is a string, and you need numbers
Indeed shows salary as human text, when it shows it at all. You'll see $90,000 - $120,000 a year, $45 an hour, $1,800 a week, Up to $130,000 a year, and plenty of blanks. To compare anything you parse three things — bounds, currency, period — then normalize to an annual figure:
| Raw string | low | high | period | annualized (low/high) |
|---|---|---|---|---|
| $90,000 - $120,000 a year | 90000 | 120000 | year | 90,000 / 120,000 |
| $45 an hour | 45 | 45 | hour | ~93,600 (x 2,080) |
| $1,800 a week | 1800 | 1800 | week | ~93,600 (x 52) |
| Up to $130,000 a year | null | 130000 | year | null / 130,000 |
Use 2,080 work-hours and 52 weeks per year as your multipliers, flag single-value strings (treat low = high), and store the original string alongside the parsed numbers so you can audit anything that looks off.
Problem 3 — freshness, or your dataset rots silently
Job data has a short shelf life. A role open three weeks ago may be filled. datePosted is how you keep the set honest: filter to a rolling window (say, posted within 14 days) for "currently hiring" use cases, and re-crawl on a schedule so you're tracking the flow of new postings rather than a stale snapshot. Resolve the relative "X days ago" string to an absolute date at scrape time, because "3 days ago" means something different every day you re-read the file.
Two jobs this data does well
Running the scrape as an API call
curl -X POST "https://api.apify.com/v2/acts/renzomacar~indeed-jobs/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"position": "react developer",
"location": "Austin, TX",
"maxItems": 200,
"maxDaysOld": 14,
"parseCompanyDetails": true
}'
You get one JSON record per listing with the fields above. Pipe it into your dedup and salary-parse steps, then append &format=csv on the dataset URL to hand a flat file to recruiters or analysts.
Gotchas worth knowing before you start
- Anti-bot is real. Indeed challenges datacenter IPs and rotates page structure. A maintained actor handles proxies and parser upkeep so your crawl doesn't go dark after a redesign.
- "Remote" is a location and a filter. Decide whether remote roles belong in a city's count before you aggregate, or your geography stats double-count.
- Sponsored vs organic. Promoted listings get extra placement. If you're measuring true hiring volume, don't let ad density skew the sample.
- Don't crawl the whole site. Scope each run to a role and location. Broad, unscoped crawls are slow, expensive and easy to get rate-limited on.
Skip the parser maintenance — get clean Indeed records
Our maintained Indeed Jobs scraper returns title, company, location, salary, type, datePosted and full descriptions as JSON or CSV, with proxies and anti-bot handled. You bring the dedup and salary logic; we keep the crawl alive. Free Apify credits to start.
Get the Indeed Scraper → Or get done-for-you leadsDisclosure: the links to Apify on this page are affiliate links. If you create a paid account through them we may earn a commission, at no extra cost to you. We recommend Apify because we build and ship the Indeed Jobs scraper on it ourselves.