The short answer
Indeed has no API that hands job listings to a third party. Its partner docs, read on November 8, 2026, offer APIs to post and manage your own jobs, a JavaScript plugin that shows Indeed jobs on a partner's site, and an MCP server in beta that works only as a Claude connector, signed in with an Indeed account. To get the job postings themselves, with the full description, the salary the employer posted, remote or hybrid, the company's rating and size, and the posting dates, our Indeed Jobs Scraper reads them from a keyword and a location, or from the search links you already use, in 62 countries. Three cities of “data analyst” jobs, 157 postings, took 16 seconds.
What Indeed offers today
From Indeed's partner docs Job postings (updated October 7, 2026), Job Sync API (October 2, 2026), its FAQ (October 6, 2026) and Indeed MCP (September 12, 2026), read on November 8, 2026:
| Option | What you get | Catch |
|---|---|---|
| Job Sync API | A GraphQL API “for ATS partners to create, upsert, expire, and check the status of job postings on Indeed.” Its listing calls return your postings: findEmployerJobsPartner “lists all of an employer's job postings.” | Your own postings only. Credentials come at onboarding, and the integration is submitted to Indeed for review. |
| Job Update API | For agencies, to “list and modify jobs that ATS integrations already publish to Indeed.” | Only jobs already published through an ATS integration. |
| Publisher JavaScript Plugin | For publishing partners, to “show Indeed jobs on your site with front-end integration only.” | The jobs are displayed on a page. The docs describe no way to receive them as data. |
| Indeed MCP server | Job search, job details, company data and the user's own resume, for an AI assistant. It “is available only for Claude Connector.” | In beta. Each user signs in with an Indeed account, inside Claude. It is made for a job seeker, not for a dataset. |
| Our Indeed Jobs Scraper | Every job of a search, newest first, with the full description, the posted salary, job types, remote or hybrid, the company's rating, reviews, size, revenue and website, the location with coordinates, and the dates. 62 country sites. | Paid per job delivered. Recruiter names and phone numbers are removed from the text, well but not perfectly. Indeed's terms and privacy law apply to how you use the data. |
Compare one job across three cities
This script reads the “data analyst” jobs posted in the last 7 days around New York, Austin and Chicago, and prints for each city the number of jobs, the share with a salary posted by the employer, the median salary turned into a yearly figure, the share of remote or hybrid jobs, the share you can apply to on Indeed directly, and the employer with the most postings:
import statistics
from collections import Counter
from decimal import Decimal
from apify_client import ApifyClient
PER_YEAR = {"hour": 2080, "day": 260, "week": 52, "month": 12, "year": 1}
CITIES = ["New York, NY", "Austin, TX", "Chicago, IL"]
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("quarra/indeed-jobs-scraper").call(
run_input={
"keywords": ["data analyst"],
"locations": CITIES,
"country": "US",
"postedWithin": "7 days",
},
max_total_charge_usd=Decimal("0.50"),
)
jobs = [j for j in client.dataset(run.default_dataset_id).iterate_items()
if j["type"] == "job" and j["status"] == "ok"]
def yearly(j):
low, high = j["salaryMin"], j["salaryMax"] or j["salaryMin"]
if low is None or j["salaryIsEstimated"] or j["salaryPeriod"] not in PER_YEAR:
return None
return (low + high) / 2 * PER_YEAR[j["salaryPeriod"]]
def percent(part, whole):
return round(100 * part / whole) if whole else None
def median_salary(js):
s = [y for j in js if (y := yearly(j)) is not None]
return f"${round(statistics.median(s), -3):,.0f}" if s else None
rows = [
("Jobs", len),
("% with a posted salary", lambda js: percent(sum(yearly(j) is not None for j in js), len(js))),
("Median yearly salary", median_salary),
("% remote or hybrid", lambda js: percent(sum(j["workplaceType"] is not None for j in js), len(js))),
("% easy apply", lambda js: percent(sum(j["easyApply"] for j in js), len(js))),
("Top employer", lambda js: Counter(j["company"] for j in js).most_common(1)[0][0] if js else None),
]
by_city = {c: [j for j in jobs if j["input"].endswith("| " + c)] for c in CITIES}
print(f"{'':24}" + "".join(f"{c:>16}" for c in CITIES))
for label, fn in rows:
print(f"{label:24}" + "".join(f"{str(fn(js))[:15]:>16}" for js in by_city.values()))
Tested with apify-client 3.4 on November 8, 2026: 157 jobs in 16 seconds, the output below. max_total_charge_usd keeps the run under $0.50 whatever the searches return. With apify-client 2.x, write run["defaultDatasetId"] instead of run.default_dataset_id.
| Data analyst, posted November 1 to 8, 2026 | New York | Austin | Chicago |
|---|---|---|---|
| Jobs | 92 | 27 | 38 |
| % with a posted salary | 98 | 56 | 84 |
| Median yearly salary | $103,000 | $92,000 | $118,000 |
| % remote or hybrid | 26 | 11 | 21 |
| % easy apply | 33 | 44 | 21 |
| Top employer | Capgemini | Apple | GEICO |
A few things the table does not say by itself. Each city is searched with Indeed's default radius, 25 miles, so it takes in the towns around it. Each job's input field carries the search that found it (data analyst | Austin, TX), which is how the script splits the cities; a job found by two searches is delivered and charged once. The salary is the midpoint of the range the employer posted (a salary posted as “From $18 an hour” counts as $18), turned into a year at 2,080 hours, 260 days, 52 weeks or 12 months; Indeed's own estimates (salaryIsEstimated: true) are left out. And 27 jobs in Austin is a small sample: widen postedWithin to 30 days before you read a gap of a few thousand dollars.
Only the new jobs, every day
Turn on onlyNew and give the watch a name: each run delivers only the jobs this watch has not delivered before, and the memory is kept in your Apify account, so your script keeps nothing. This one appends the new jobs to a file and prints how many came in. Schedule it once a day:
import json
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("quarra/indeed-jobs-scraper").call(run_input={
"keywords": ["forklift driver"],
"locations": ["Houston, TX"],
"country": "US",
"postedWithin": "3 days",
"onlyNew": True,
"watchName": "houston-forklift",
})
new = [j for j in client.dataset(run.default_dataset_id).iterate_items()
if j["type"] == "job" and j["status"] == "ok"]
with open("new-jobs.jsonl", "a", encoding="utf-8") as f:
for j in new:
f.write(json.dumps(j, ensure_ascii=False) + "\n")
print(len(new), "new jobs,",
sum(j["salaryMin"] is not None and not j["salaryIsEstimated"] for j in new), "with a posted salary,",
sum(j["easyApply"] for j in new), "with easy apply")
On November 8, 2026, two executions about 10 seconds apart. The first printed 72 new jobs, 39 with a posted salary, 45 with easy apply: on the first run of a watch, every matching job is new. The second printed 0 new jobs, 0 with a posted salary, 0 with easy apply. It read back only to 48 hours before the previous complete reading, a margin for jobs Indeed adds late: 36 jobs, none it had not delivered. It paid only its start, $0.0009 on the Free plan. The first run cost $0.0074. A daily schedule pays one start a day plus $0.09 per 1,000 new jobs. Watch names are case-sensitive, and a watch takes one run at a time.
To have the new jobs land in Slack or Discord without opening Apify, add "deliveryWebhookUrl" with a Slack Incoming Webhook or a Discord channel webhook: after each run it receives the count, the first 25 jobs with their links and a link to the whole dataset.
Several countries, or the search links you already have
Indeed has one site per country. Paste search pages from any of them, filters included, and they run together:
from collections import Counter
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("quarra/indeed-jobs-scraper").call(run_input={
"startUrls": [
"https://uk.indeed.com/jobs?q=data+analyst&l=London&fromage=3",
"https://de.indeed.com/jobs?q=Pflegefachkraft&l=Berlin&radius=10",
],
"maxJobsPerSearch": 200,
})
jobs = [j for j in client.dataset(run.default_dataset_id).iterate_items()
if j["type"] == "job" and j["status"] == "ok"]
for country in sorted({j["countryCode"] for j in jobs}):
js = [j for j in jobs if j["countryCode"] == country]
print(country, len(js), "jobs |",
"cities:", Counter(j["city"] for j in js).most_common(3), "|",
"languages:", dict(Counter(j["language"] for j in js)), "|",
"salary currencies:", dict(Counter(j["salaryCurrency"] for j in js if j["salaryCurrency"])), "|",
"oldest:", min(j["postedAt"] for j in js)[:16])
On November 8, 2026, this run took 18 seconds and printed:
DE 200 jobs | cities: [('Berlin', 160), ('Berlin-Wedding', 13), ('Berlin-Lichtenberg', 8)] | languages: {'de': 200} | salary currencies: {'EUR': 100} | oldest: 2026-10-15T16:34
GB 51 jobs | cities: [('London', 47), ('St Albans', 1), ('South Kensington', 1)] | languages: {'en': 51} | salary currencies: {'GBP': 14} | oldest: 2026-11-05T17:56
The London link kept its 3-day filter (fromage=3): no job older than November 5. The Berlin search stopped at 200 and the run said so in its status (“1 search reached 'Max jobs per search' (200); raise it to get more”). Without maxJobsPerSearch, the same Berlin search had delivered 949 jobs, back to February 2026, when maxItems, 1,000 by default for the whole run, stopped it. Raise maxItems to read a search to its last job: “nurse” in the United States gave 3,999 jobs on November 8, 2026.
Limits
- Recruiter contacts are removed from
descriptionandcompanyDescription: a phone number, a personal e-mail address, or a recruiter's name near them is replaced by[removed]. On 3,116 jobs it had never seen, from 32 country sites, 0.3% still had a recruiter's name in the eight languages it was tuned on, and 1.7% still had a name or a phone number on the other sites. Treat the text as text that may still hold a name. - Indeed lists most remote jobs without a city. Remote with a location keeps only the remote jobs filed there: for every remote job, set
workplaceto Remote and leavelocationsempty. - The salary is the one the employer posted. When there is none and Indeed shows an estimate, you get the estimate with
salaryIsEstimated: true; with neither, the salary fields arenull. - How much a country site fills in varies: in our runs on Japan's site, about one job in ten carries the company's rating (89% in the United States), because Indeed shows little of it there.
- Up to 100 keywords and 100 locations per run, 500 searches in all, and 20 countries, links included.
- A search cut short is named in the run's status, gets a free error record and is counted in
incompleteSearchesin the run summary. A watch run that stops early is continued by the next run of the same watch. - Indeed's terms and privacy law apply to how you use the data.
What it costs
| Apify plan | Per 1,000 jobs | Per run start |
|---|---|---|
| Free | $0.09 | $0.0009 |
| Starter | $0.08 | $0.0008 |
| Scale | $0.07 | $0.0007 |
| Business and above | $0.06 | $0.0006 |
The Actor runs with 256 MB of memory, so a run pays one start. The 157 jobs of the three cities cost $0.015 on the Free plan and $0.010 on Business. A job found by more than one search, jobs already delivered to your watch, jobs closed before their page was read and error records are not charged.
Questions
Does Indeed have a job search API?
Not one that returns job listings to you. Its partner docs offer the Job Sync API and the Job Update API to manage postings, a Publisher JavaScript Plugin that displays jobs on a partner's site, and an MCP server in beta that works only as a Claude connector, signed in with an Indeed account.
Can I get every job of a search, not just the first pages?
Yes. Leave maxJobsPerSearch empty and raise maxItems. A search is read to its last job, newest first: “nurse” in the United States gave 3,999 distinct jobs on November 8, 2026.
Do I get the full description?
Yes, as plain text with one paragraph or list item per line. On those 3,999 nurse jobs, every record had it, with a median of 3,659 characters.
Do I need an Indeed account or an API key?
No. You need an Apify account and its API token.
Run it on the Apify Store
Jobs cost $0.09 per 1,000 on the Free plan ($0.08 on Starter, $0.07 on Scale, $0.06 on Business and above), plus $0.0009 per run start. A job found by two searches is charged once, jobs already delivered to your watch and error records are not charged, and you can set a maximum cost per run.