Field guide · 2026

There is no Indeed jobs API. There's a search page, three data problems, and a way through them.

Indeed retired its open Publisher search API years ago. Collecting listings now means scraping — and the hard part isn't the scrape, it's the reposts, the salary strings, and the freshness.

Open the Indeed Actor →

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:

title
Job title as posted — noisy, often padded with seniority and location.
company
Employer name. Needs normalizing; "Acme Inc." and "Acme, Inc" are the same firm.
location
City/state, or "Remote", or a hybrid string. Parse it before you group by geography.
salary
Free text when present at all. The single messiest field — see below.
jobType
Full-time, contract, part-time, etc. Often a list, not a single value.
datePosted
Relative ("3 days ago") on the card, resolvable to an absolute date. Your freshness lever.
description
Full body from the detail page. The richest field for skills and seniority extraction.
jobKey
Indeed's per-listing ID. Useful, but do NOT dedup on it — reposts get new keys.

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 stringlowhighperiodannualized (low/high)
$90,000 - $120,000 a year90000120000year90,000 / 120,000
$45 an hour4545hour~93,600 (x 2,080)
$1,800 a week18001800week~93,600 (x 52)
Up to $130,000 a yearnull130000yearnull / 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

Labor-market intelligenceHiring volume, salary bands and title trends by role, company or metro — once deduped and salary-normalized.
Recruiting and BD signalsA company posting 12 sales roles this month is a buying signal for anything sales-adjacent. Roles map to growth.

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

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 leads

Disclosure: 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.