Indeed Jobs Scraper
Pricing
from $5.00 / 1,000 results
Indeed Jobs Scraper
Extract detailed job listings from Indeed by search query + filters or by URL. Returns ~60 structured fields per job plus cross-run deduplication, skill/seniority extraction, and normalized salaries.
Pricing
from $5.00 / 1,000 results
Rating
0.0
(0)
Developer
Mukesh Kumar
Maintained by CommunityActor stats
0
Bookmarked
4
Total users
2
Monthly active users
10 hours ago
Last modified
Categories
Share
Indeed Jobs Scraper — Extract Job Listings, Salaries & Skills from Indeed.com
Indeed Jobs Scraper is a fast, structured Indeed scraper that extracts job postings from Indeed.com and 50+ Indeed country domains (UK, Canada, Australia, India, Germany, France, and more). Search by keyword + filters or paste any Indeed URL and get ~60 structured fields per job as JSON, CSV, Excel, or XML — complete with salary normalization, skills extraction, seniority detection, and cross-run deduplication for scheduled monitoring.
Use it to build job boards, recruiter dashboards, salary benchmarks, hiring intelligence, and ATS pipelines — without writing a single line of scraping code.
Not affiliated with, endorsed by, or sponsored by Indeed. Scrape responsibly and in compliance with applicable laws and Indeed's terms of service.
Why this Indeed scraper
- Two input modes — supply a list of Indeed URLs or a search query + country + filters. Mix-and-match for any use case.
- 50+ country domains —
indeed.com,uk.indeed.com,ca.indeed.com,de.indeed.com,au.indeed.com,in.indeed.com, and more. - Full job details — title, company, location, salary, benefits, full description (text + optional HTML), apply URL, posting date, urgent-hire signals.
- Skills & seniority extraction — automatic parsing of
skills[],detectedSeniority(entry / mid / senior), andminYearsExperience. - Normalized salaries — annualized min/max converted to one target currency (USD, EUR, GBP, INR, CAD, AUD, and more) with optional FX overrides.
- Cross-run deduplication — emit only new jobs since the last run, perfect for scheduled job monitoring (hourly / daily / weekly).
- Webhook delivery — push new postings to Slack, Make, Zapier, n8n, or your own endpoint on every run finish.
- Cloudflare-aware — fingerprinted browser over Apify residential proxies with session rotation and challenge detection.
- Exports everywhere — JSON, CSV, Excel, XML, HTML, or live via the Apify API.
Use cases
| Use case | What you get |
|---|---|
| Job board / aggregator | A live feed of Indeed listings with full details, refreshed on a schedule |
| Recruitment intelligence | Track competitor hiring — which companies post which roles, where, at what salary |
| Salary benchmarking | Normalized annual min/max across roles, locations, and seniority levels |
| ATS / sourcing pipelines | Push new postings into your ATS, CRM, or Airtable via webhook + API |
| Market & labor analytics | Build dashboards on hiring volume, remote-work trends, in-demand skills |
| Lead generation | Find companies that are actively hiring for specific tech stacks or roles |
| Personal job alerts | Cross-run dedupe + webhook = a private, customized job alert in Slack/email |
Quick start
Option A — Scrape by Indeed URL
Paste any Indeed search, company, or filtered URL:
{"urls": ["https://www.indeed.com/jobs?q=software+engineer&l=Remote","https://www.indeed.com/cmp/Google/jobs","https://uk.indeed.com/jobs?q=data+scientist&l=London"],"maxRowsPerUrl": 50,"scrapeJobDetails": true}
Option B — Scrape by search query + filters
{"query": "data analyst","country": "us","location": "New York, NY","radius": "25","jobType": "fulltime","level": "mid_level","sort": "date","fromDays": "7","remote": "remote","maxRows": 100,"scrapeJobDetails": true,"extractSkills": true,"normalizeSalary": true,"targetCurrency": "USD"}
Input schema
Provide either urls (Option A) or query + country (Option B). All other fields are optional.
Option A — by URL
| Field | Type | Default | Description |
|---|---|---|---|
urls | string[] | [] | Indeed search, company, or filtered URLs. When set, query-based fields are ignored. |
maxRowsPerUrl | integer | 100 | Maximum jobs to scrape per URL. 0 = no limit. |
Option B — by search parameters
| Field | Type | Default | Description |
|---|---|---|---|
query | string | — | Job keywords, e.g. "data analyst". Required when not using urls. |
country | enum | us | Indeed country domain. 50+ options including us, gb, ca, au, in, de, fr, es, it, nl, br, mx, jp, sg, ae, and more. |
location | string | — | City, state, or postcode, e.g. "San Francisco, CA". |
radius | enum | — | Miles from location: 0, 5, 10, 15, 25, 35, 50, 100. |
jobType | enum | — | fulltime, parttime, contract, internship, temporary, permanent, seasonal, freelance. |
level | enum | — | entry_level, mid_level, senior_level. |
sort | enum | relevance | relevance or date (newest first). |
fromDays | enum | — | Posted within 1, 3, 7, or 14 days. |
remote | enum | — | remote or hybrid. |
maxRows | integer | 100 | Total cap across all pages. 0 = no limit. |
includeSimilarJobs | boolean | true | Include Indeed's "similar to your search" suggestions. |
Enrichment & advanced
| Field | Type | Default | Description |
|---|---|---|---|
scrapeJobDetails | boolean | true | Visit each job page for the full description, benefits, apply URL, and company profile. Disable for a faster listing-only run. |
includeDescriptionHtml | boolean | false | Include the raw HTML job description in addition to plain text. |
extractSkills | boolean | true | Parse the description into skills[], detectedSeniority, and minYearsExperience. |
normalizeSalary | boolean | true | Add normalizedSalary{} — annualized min/max in a single target currency. |
targetCurrency | enum | USD | Output currency: USD, EUR, GBP, CAD, AUD, INR, SGD, ZAR, BRL, MXN, AED, JPY, CHF, SEK, PLN. |
fxRatesOverride | object | {} | Override built-in FX rates (units per 1 USD), e.g. {"EUR": 0.92, "GBP": 0.79}. |
deduplicate | boolean | true | Drop duplicate jobKeys within this run. |
onlyNewSinceLastRun | boolean | false | Cross-run dedupe — persist seen jobKeys and emit only postings unseen in prior runs. |
dedupeStoreName | string | indeed-seen-jobs | Named key-value store used for cross-run deduplication. |
notifyWebhookUrl | string | — | Optional. POST a JSON summary of the run's jobs to this URL on finish. Secret. |
maxConcurrency | integer | 5 | Max parallel browser pages (1–25). Lower = gentler on anti-bot systems. |
proxyConfiguration | object | RESIDENTIAL | Proxy settings. Residential proxies strongly recommended — Indeed blocks datacenter IPs. |
Output schema
One record per job is pushed to the default dataset. Below is the full structure with all fields and types.
Example output record
{"jobKey": "a1b2c3d4e5f67890","title": "Senior Data Analyst","companyName": "Acme Corp","companyUrl": "https://www.indeed.com/cmp/Acme-Corp","companyLogoUrl": "https://d2q79iu7y748jz.cloudfront.net/s/_logo/acme.png","companyRating": 4.1,"companyReviewCount": 320,"city": "New York","state": "NY","postalCode": "10001","formattedLocation": "New York, NY 10001","isRemote": false,"jobType": "Full-time","salary": {"salaryText": "$90,000 - $120,000 a year","salaryMin": 90000,"salaryMax": 120000,"currency": "USD","period": "year"},"benefits": ["Health insurance", "401(k)", "Paid time off"],"descriptionText": "We are looking for a Senior Data Analyst with 5+ years of SQL and Python experience...","descriptionHtml": "<p>We are looking for...</p>","datePublished": "2026-05-12T00:00:00.000Z","age": "3 days ago","sponsored": false,"hiringDemand": {"isUrgentHire": true,"isHighVolumeHiring": false},"jobUrl": "https://www.indeed.com/viewjob?jk=a1b2c3d4e5f67890","applyUrl": "https://www.indeed.com/applystart?jk=a1b2c3d4e5f67890","expired": false,"skills": ["SQL", "Python", "Tableau", "Data Analysis", "Statistics"],"detectedSeniority": "senior","minYearsExperience": 5,"normalizedSalary": {"annualMin": 90000,"annualMax": 120000,"currency": "USD","sourceCurrency": "USD","sourcePeriod": "year","fxRateApplied": 1,"isEstimatedFromText": false},"source": "indeed","scrapedAt": "2026-05-18T10:00:00.000Z"}
Field reference
Core fields
| Field | Type | Description |
|---|---|---|
jobKey | string | Indeed's unique job identifier (used in viewjob?jk=…). |
title | string | Job title. |
companyName | string | Hiring company. |
companyUrl | string | Indeed company profile URL. |
companyLogoUrl | string | null | Company logo image URL. |
companyRating | number | null | Company rating on Indeed (0–5). |
companyReviewCount | number | null | Number of Indeed reviews. |
city | string | null | City. |
state | string | null | State / region code. |
postalCode | string | null | Postal / ZIP code. |
formattedLocation | string | Human-readable location string. |
isRemote | boolean | Whether the role is remote / hybrid. |
jobType | string | null | Employment type — Full-time, Contract, etc. |
Salary, benefits, description
| Field | Type | Description |
|---|---|---|
salary.salaryText | string | null | Raw salary text from Indeed. |
salary.salaryMin | number | null | Parsed minimum (source currency, source period). |
salary.salaryMax | number | null | Parsed maximum (source currency, source period). |
salary.currency | string | null | Source currency code (e.g. USD, GBP, EUR). |
salary.period | string | null | Pay period — hour, day, week, month, year. |
benefits | string[] | Listed benefits, e.g. ["Health insurance", "401(k)"]. |
descriptionText | string | Plain-text job description. |
descriptionHtml | string | null | Raw HTML description (only when includeDescriptionHtml: true). |
Dates, metadata, links
| Field | Type | Description |
|---|---|---|
datePublished | string (ISO 8601) | When the job was first posted on Indeed. |
age | string | Human-readable age, e.g. "3 days ago". |
sponsored | boolean | Whether this is a sponsored / promoted listing. |
hiringDemand.isUrgentHire | boolean | Indeed's "Urgently hiring" badge. |
hiringDemand.isHighVolumeHiring | boolean | High-volume hiring signal. |
jobUrl | string | Canonical Indeed job URL. |
applyUrl | string | null | Direct apply URL (Indeed Apply or external ATS). |
expired | boolean | Whether the posting is expired / no longer accepting applications. |
source | string | Always "indeed". |
scrapedAt | string (ISO 8601) | Timestamp of when this record was collected. |
Enrichment fields (when enabled)
| Field | Type | Description |
|---|---|---|
skills | string[] | Detected skills from title + description (e.g. SQL, Python, AWS, React). |
detectedSeniority | string | null | entry, mid, or senior. |
minYearsExperience | number | null | Minimum years of experience parsed from the description. |
normalizedSalary.annualMin | number | null | Annualized minimum in targetCurrency. |
normalizedSalary.annualMax | number | null | Annualized maximum in targetCurrency. |
normalizedSalary.currency | string | Output currency (defaults to USD). |
normalizedSalary.sourceCurrency | string | Original currency on the posting. |
normalizedSalary.sourcePeriod | string | Original period (hour, year, etc.). |
normalizedSalary.fxRateApplied | number | FX rate used for conversion. |
normalizedSalary.isEstimatedFromText | boolean | true if the salary was parsed from free-text vs structured data. |
Scheduled monitoring — get only new Indeed jobs
Set up an automated Indeed job monitor that delivers only fresh postings:
- Enable
"onlyNewSinceLastRun": trueand pick adedupeStoreName. - Set
"notifyWebhookUrl"to your Slack incoming webhook, Make/Zapier scenario, n8n workflow, or any HTTPS endpoint. - Schedule the actor (hourly / daily) from the Apify Console → Schedules.
- Each run's dataset (and webhook payload) contains only postings unseen in prior runs.
Webhook payload (POST, application/json)
{"event": "indeed.jobs.scraped","finishedAt": "2026-05-18T10:00:00.000Z","runId": "abc123","datasetId": "xyz789","datasetItemsUrl": "https://api.apify.com/v2/datasets/xyz789/items?format=json","source": {"query": "data analyst","country": "us","location": "New York, NY"},"monitoring": true,"totalJobs": 18,"delivered": 18,"truncated": false,"jobs": [{"title": "Senior Data Analyst","companyName": "Acme Corp","salaryText": "$90,000 - $120,000 a year","normalizedSalary": {"annualMin": 87692,"annualMax": 131538,"currency": "USD"},"skills": ["SQL", "Python"],"jobUrl": "https://www.indeed.com/viewjob?jk=...","applyUrl": "https://..."}]}
Up to 200 jobs are inlined; if truncated is true, fetch the rest from datasetItemsUrl. Test with a free webhook.site URL before connecting production endpoints.
Output formats & integrations
Pull results as:
- JSON —
https://api.apify.com/v2/datasets/{DATASET_ID}/items?format=json - CSV —
?format=csv - Excel —
?format=xlsx - XML / HTML / JSONL — same dataset endpoint, different
formatparam
Native integrations: Make, Zapier, n8n, Slack, Airtable, Google Sheets, Pipedream, plus generic webhooks.
Pricing
This actor is priced per scraped job result via Apify's built-in pay-per-result billing.
- Measured Apify usage — approximately $6–7 per 1,000 jobs of platform cost (browser compute + residential proxy bandwidth).
- Recommended retail price — $8–12 per 1,000 jobs to maintain margin, or compete on enrichment + monitoring differentiators rather than raw price.
- Re-measure with
apify runafter any change before locking the production price.
The pay-per-event configuration lives in .actor/pay_per_event.json. Final pricing is set in Apify Console → Publication → Monetization.
Realistic expectations & limits
Indeed is Cloudflare-protected and aggressively rate-limits deep pagination. This actor uses a fingerprinted headless browser over Apify residential proxies — the most reliable self-contained approach. Plan accordingly:
- Dependable yield — ~1 results page per search (~15–25 jobs) plus full job details. Deeper pagination is attempted, but treat page 1 as the reliable floor.
- Best fits — scheduled monitoring (only-new-since-last-run), niche / targeted queries, salary and skill intelligence.
- Less suited for — bulk multi-thousand-job harvests in a single run. For that, run multiple narrower queries on a schedule.
How it works
A single PlaywrightCrawler with:
- Browser fingerprint injection to look like real users.
- Apify residential proxies with session rotation per blocked request.
- Cloudflare challenge detection — interstitials (title, page markers, status codes) are detected and the session is retired and retried on a fresh fingerprint.
- Bandwidth control — images, fonts, media, and trackers are blocked at the network layer.
Parsing strategy
- Listing pages — extracts
window.mosaic.providerData["mosaic-provider-jobcards"]JSON via a brace-matching extractor, with explicit "no results" vs "soft block" detection. - Detail pages — deep-searches
window._initialDatafor the job model (resilient to Indeed reshaping JSON paths), with<title>+ regex fallbacks. - Pagination —
startparameter is stepped by Indeed's real page size (15). Stops at the last (short) page instead of hammering a dead offset and triggering blocks.
If Indeed changes its private remote/hybrid filter codes, the actor falls back to a query keyword so runs don't fail outright.
Local development
git clone <this-repo>cd indeed-jobs-scrapernpm install# Edit your test input:# storage/key_value_stores/default/INPUT.jsonnpx apify run # or: npm start
Requirements: Node.js 20+, Docker (for Apify build), and an Apify account if you want to deploy.
FAQ
Is this Indeed scraper legal?
The actor scrapes only publicly accessible Indeed job postings. You are responsible for using the data in compliance with applicable laws (GDPR, CCPA, etc.) and Indeed's terms of service. This actor is not affiliated with Indeed.
Why are residential proxies required?
Indeed is protected by Cloudflare and blocks datacenter IPs almost instantly. The default Apify residential proxy group is strongly recommended — without it, runs will fail with Cloudflare challenges.
Can I scrape Indeed UK, Canada, Germany, or India?
Yes. Set the country field to any of 50+ supported domains (gb, ca, de, in, fr, au, etc.), or paste country-specific Indeed URLs directly into urls.
How do I get only new jobs since my last run?
Enable "onlyNewSinceLastRun": true and set a dedupeStoreName. Pair with a schedule and a notifyWebhookUrl for a fully automated Indeed job alert in Slack, Make, Zapier, or your own system.
Does it return salary data?
Yes — both raw salary text (salary.salaryText) and normalized annualized salaries (normalizedSalary.annualMin/annualMax) converted to your chosen targetCurrency. Hourly, weekly, and monthly rates are auto-annualized.
Can it extract skills from job descriptions?
Yes — enable extractSkills (default true). The actor parses the title + description into a skills[] array, plus detected seniority and minYearsExperience.
How fast is the actor?
With defaults (maxConcurrency: 5, full job details enabled), expect ~3–6 seconds per job end-to-end (browser load + Cloudflare wait + detail parse). Disable scrapeJobDetails for a 5× faster listing-only run.
What if Indeed changes its HTML?
Parsing reads embedded JSON (window.mosaic.providerData, window._initialData) rather than DOM selectors, which is far more resilient to HTML reshuffles. Where regex fallbacks exist, they are documented in src/parsers/.
Can I get the full HTML description?
Yes — set "includeDescriptionHtml": true to receive descriptionHtml alongside descriptionText.
Related Apify scrapers
- LinkedIn Jobs Scraper — extract jobs from LinkedIn
- Glassdoor Jobs Scraper — jobs + company reviews + salary data
- ATS Jobs Scraper — direct from Greenhouse, Lever, Ashby, Workday career pages
- Job Posting Aggregator — combine Indeed + LinkedIn + Glassdoor into one feed
Support
Open an issue or contact the maintainer via the Apify Store actor page. Bugs reproduced with a public input JSON + a non-blocked URL are the fastest to resolve.
Tags: indeed scraper, indeed jobs scraper, scrape indeed, indeed api, job scraper, jobs api, salary data, recruitment data, hiring intelligence, job board scraper, indeed.com scraper, job listings scraper, ATS feed, job aggregator, indeed UK, indeed Canada, indeed India.