IndeedAnswer

Does Indeed have an API? Job listings and salaries as JSON

Indeed has no public job search API; its APIs serve employers and ATS integrations. Ours returns about 45 jobs a search with salaries, 1 credit a call.

6 min read

Short answer

  • Indeed has no public job search API. Its APIs are for employers and ATS integrations, and the old Publisher API host no longer resolves.
  • ToolzerHub's /v1/indeed/jobs/search returns about 45 jobs per plain search with company, structured salary, job type and posting date, at 1 credit per call.
  • Indeed shows one page per search, so a script runs 11 variants of a search. It saved 147 data analyst jobs in New York and 140 electrician jobs in Houston.
On this page

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, radius and sort=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.
bash
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:

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

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).

python
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_ago if 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.

Get this data from your own code

The Indeed API returns it as JSON with one x-api-key header. 100 free credits monthly.