Indeed has no public API for job listings. Its APIs are for employers and applicant tracking systems (ATS), and the old job search host no longer resolves. To pull Indeed jobs and salaries as data, use our Indeed API; the script below saves a search to CSV.
What Indeed's API is for
Indeed's developer APIs let employers and ATS integrations post jobs and receive applications. Older tutorials call the Publisher API at api.indeed.com/ads/apisearch with a publisher ID, but that host stopped resolving. None of it searches jobs for you. If you run an ATS or post jobs for employers, use Indeed's own integrations; ours reads the public side, such as a job alert, a pay check for a role or a list of who is hiring.
Indeed jobs with ToolzerHub
We make ToolzerHub. Our Indeed API has four routes, each a GET with the x-api-key header at 1 credit a call:
/v1/indeed/jobs/search?query=...&location=...returns one page of jobs. Optional filters:days_ago(1, 3, 7 or 14),job_type,radiusandsort=date./v1/indeed/jobs/detail?jk=...returns one job in full, with the description, employment type and the date it expires./v1/indeed/salaries?title=...returns Indeed's pay data for a job title: national mean, percentiles, state averages and top-paying companies./v1/indeed/company?slug=...returns an employer's Indeed page with its rating and reviews.
export TOOLZERHUB_API_KEY=your_key
curl -G -H "x-api-key: $TOOLZERHUB_API_KEY" \
--data-urlencode "query=data analyst" --data-urlencode "location=New York, NY" \
"https://api.toolzerhub.com/v1/indeed/jobs/search"
The first job on the page, inside data.results, trimmed to the fields the script uses:
{
"job_key": "23869ce3177273e4",
"title": "Regulatory Data Analyst",
"company": { "name": "Federal Reserve Bank of New York", "rating": 4.1, "review_count": 575 },
"location": { "formatted": "New York, NY", "city": "New York", "state": "NY" },
"salary": { "min": 79300, "max": 85000, "period": "YEARLY", "text": "$79,300 - $85,000 a year" },
"job_types": ["Full-time"],
"sponsored": false,
"published_at": "2026-10-21T05:00:00.000Z"
}
Each result also carries the snippet, benefits, apply links and Indeed's ad fields. The salary comes split into min, max and period, so you don't parse "$79,300 - $85,000 a year" yourself. An employer with no Indeed reviews has a rating of 0.
One page per search
A search returns one page: 42 to 46 jobs for a plain search, fewer with filters (19 for jobs from the last day). More jobs come from more searches, because the same query with a filter changed returns a different page. "Data analyst" in New York run 13 ways gave 178 distinct jobs, against 46 for the plain search.
The job type filter is loose. With job_type=contract, 15 of 43 jobs were labelled contract, against 5 of 46 in the plain search.
Save Indeed jobs to CSV
The script runs 11 variants of one search, skips jobs it has already written, and saves one row per job with the company, rating, salary range and period, job types, posting date and the variant that found it. Save it as indeed_jobs.py and install requests (pip install requests).
import csv
import os
import re
import sys
import time
import requests
URL = "https://api.toolzerhub.com/v1/indeed/jobs/search"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
# Indeed serves one page per search, so each slice is the same search with one filter changed.
SLICES = [("relevance", {}), ("newest", {"sort": "date"})]
JOB_TYPES = ("fulltime", "parttime", "contract", "temporary", "internship")
SLICES += [(t, {"job_type": t}) for t in JOB_TYPES]
SLICES += [(f"last_{d}_days", {"days_ago": d}) for d in (1, 3, 7, 14)]
FIELDS = ["job_key", "title", "company", "rating", "location", "sponsored", "salary_min",
"salary_max", "salary_period", "job_types", "published", "found_by", "url"]
def search(params, tries=3):
# 429, 5xx and dropped connections are retried; failed calls cost no credits.
for attempt in range(tries):
last = attempt == tries - 1
try:
r = requests.get(URL, headers=HEADERS, params=params, timeout=180)
except requests.RequestException:
if last:
raise
time.sleep(10 * (attempt + 1))
continue
if r.status_code in (429, 500, 502, 503, 504) and not last:
time.sleep(10 * (attempt + 1))
continue
if r.status_code == 402:
sys.exit("out of credits: the CSV keeps the jobs written so far")
r.raise_for_status()
return (r.json().get("data") or {}).get("results") or []
def main(query, location="", out=None):
name = re.sub(r"\W+", "_", query)
out = out or f"indeed_{name}.csv"
seen = set()
with open(out, "w", newline="", encoding="utf-8-sig") as f: # -sig: Excel reads UTF-8
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for slice_name, extra in SLICES:
params = {"query": query, **({"location": location} if location else {}), **extra}
try:
jobs = search(params)
except requests.RequestException as e: # one failed variant shouldn't end the run
print(f"{slice_name}: skipped ({e})")
continue
new = 0
for job in jobs:
if not job.get("job_key") or job["job_key"] in seen:
continue
seen.add(job["job_key"])
new += 1
company, place = job.get("company") or {}, job.get("location") or {}
salary = job.get("salary") or {}
writer.writerow({
"job_key": job["job_key"], "title": job.get("title"),
"company": company.get("name"),
"rating": company.get("rating") or "", # 0 means no reviews yet
"location": place.get("formatted"), "sponsored": job.get("sponsored"),
"salary_min": salary.get("min"), "salary_max": salary.get("max"),
"salary_period": salary.get("period"),
"job_types": ", ".join(job.get("job_types") or []),
"published": (job.get("published_at") or "")[:10], "found_by": slice_name,
"url": f"https://www.indeed.com/viewjob?jk={job['job_key']}",
})
print(f"{slice_name}: {len(jobs)} jobs, {new} new")
print(f"{len(seen)} jobs from {len(SLICES)} searches written to {out}")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit('usage: python indeed_jobs.py "data analyst" ["New York, NY"] [file.csv]')
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
python indeed_jobs.py "data analyst" "New York, NY" wrote 147 jobs from 103 companies, and python indeed_jobs.py "electrician" "Houston, TX" wrote 140 from 120. The 22 calls took 25 and 29 seconds.
What the jobs show
- About 2 in 3 were sponsored: 99 of the 147 data analyst jobs and 97 of the 140 electrician jobs.
- 86% of the data analyst jobs had a salary: 95 yearly, with a median midpoint of $107,500, and 31 hourly. 70% of the electrician jobs had one, 72 of them hourly, with a median midpoint of $29 an hour.
- Some are old: 36 data analyst jobs and 31 electrician jobs were first published more than 30 days earlier. Add
days_agoif you only want new jobs.
Salary data by job title
/v1/indeed/salaries returns Indeed's own pay figures, separate from the ads. For "data analyst" with location=New York, NY it returned a mean of $93,252 a year nationally and $103,969 for New York, plus the 10th and 90th percentiles, averages by state, top-paying companies and related titles.
What it costs
A search runs 11 variants at 1 credit each, so it costs 11 credits and returned about 145 distinct jobs, about 77 credits per 1,000 jobs. The 100 free credits a month cover 9 searches; $19 buys 10,000 credits that never expire, and plans for higher volume cover more searches. Set your key and the first search is a CSV in under a minute.
For other job boards, see Wellfound startup jobs, Seek job ads as JSON for Australia and how to scrape LinkedIn for LinkedIn jobs.
We are not affiliated with Indeed.