Indeed Jobs Scraper — Detailed avatar

Indeed Jobs Scraper — Detailed

Pricing

Pay per usage

Go to Apify Store
Indeed Jobs Scraper — Detailed

Indeed Jobs Scraper — Detailed

Detailed Indeed job scraping: full descriptions, pre-parsed salaries, benefits, shifts, company ratings, normalized titles and resolved ATS apply URLs across 63 country domains. Fails loudly instead of returning an empty dataset.

Pricing

Pay per usage

Rating

0.0

(0)

Developer

Kashif Ali

Kashif Ali

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 days ago

Last modified

Share

Scrape Indeed job postings with the full description, GPS coordinates, pre-parsed salary, employer firmographics (revenue, headcount, CEO), skills and the employer's real ATS apply URL — across 63 Indeed country domains, with deep cursor pagination.

HTTP-only, no headless browser. A 200-job run costs 2 requests, because every job arrives complete — descriptions are not fetched one page at a time.

Two things no other Indeed scraper on the store does: incremental runs that return only what is new since last time, and a run that fails loudly instead of handing you an empty dataset when it was blocked.

What you get per job

{
"jobKey": "9e9e0856fe8e38c7",
"url": "https://www.indeed.com/viewjob?jk=9e9e0856fe8e38c7",
"title": "Software Development Engineering - Advisor I",
"normalizedTitle": null,
"company": "Fiserv",
"companyUrl": "https://www.indeed.com/cmp/Fiserv",
"companyRating": null,
"companyReviewCount": null,
"location": "Berkeley Heights, NJ 07922",
"city": "Berkeley Heights",
"state": null,
"country": "US",
"isRemote": false,
"salaryText": "$110,000 - $186,000 a year",
"salaryMin": 110000,
"salaryMax": 186000,
"salaryCurrency": "USD",
"salaryPeriod": "YEARLY",
"salaryIsEstimate": false,
"jobTypes": [
"Full-time"
],
"shifts": [],
"schedules": [],
"benefits": [],
"postedAt": "2026-08-24T05:00:00.000Z",
"postedRelative": null,
"scrapedAt": "2026-08-24T23:06:15.390Z",
"isSponsored": false,
"isNew": false,
"isExpired": false,
"isUrgentlyHiring": false,
"isEasyApply": false,
"applyUrl": "https://careers.fiserv.com/us/en/job/R-10401161...",
"atsUrl": "https://careers.fiserv.com/us/en/job/R-10401161",
"atsProvider": "careers.fiserv.com",
"descriptionHtml": "<div></div><div><div><div><div>Location: Berkeley Heights,...",
"descriptionText": "Location: Berkeley Heights, New Jersey, United States of America Job ID: R-10401161 Category...",
"descriptionSnippet": null,
"occupationIds": [
"5NN53",
"EHPW9",
"HJSX6"
],
"occupations": [
"Software Development Occupations",
"Technology Occupations",
"Software Development & Architecture Occupations"
],
"attributes": [
"Software engineering",
"Azure",
"Continuous Delivery (CD) implementation",
"Engineering development testing",
"Requirements design",
"C#"
],
"latitude": 40.665768,
"longitude": -74.41226,
"streetAddress": "100 Connell Drive",
"postalCode": "07922",
"language": "en",
"isUrgentHire": false,
"isHighVolumeHiring": false,
"companyIndustry": null,
"companySize": "10,000+",
"companyRevenue": "more than $10B (USD)",
"companyCeo": "Mike Lyons",
"companyAddresses": [
"Milwaukee, WI"
],
"source": "graphql",
"hiringInsights": {
"isUrgentHire": false,
"isHighVolumeHiring": false
},
"feedId": null,
"sourceId": null,
"searchQuery": "software engineer",
"searchLocation": "New York, NY"
}

That is a real, unedited row (long URLs and description text trimmed for display).

Fill rates, measured on a 200-job GraphQL run — what to expect, not what to hope for:

FieldFilled
descriptionText, latitude, longitude, attributes, isExpired, language200/200
occupations199/200
atsProvider184/200
salaryText181/200
companySize156/200
companyRevenue142/200
companyCeo111/200
postalCode99/200
streetAddress72/200
companyIndustry32/200

All 200 rows had distinct jobKeys. Firmographic gaps mean Indeed holds no company profile for that employer — see Honest limits.

Input

Search by keyword and location

{
"queries": ["software engineer"],
"locations": ["New York, NY"],
"country": "US",
"maxItems": 200
}

Indeed search operators

The queries field takes Indeed's own search syntax:

SyntaxMatches
data engineerthe words anywhere in the posting, plus related roles
"data engineer"that exact phrase anywhere in the posting
title:(data engineer)both words in the job title, any order
title:("data engineer")the exact phrase in the job title (strictest)
company:Googleevery job from one employer
nurse -travelexcludes postings containing "travel"

Combine with and / or / not and parentheses: title:(designer) -intern (remote or hybrid).

Lead generation: find companies hiring, and what they hire with

{
"queries": ["title:(\"account executive\")"],
"locations": ["Austin, TX"],
"maxItems": 500
}

Every row names the ATS behind the employer's apply link — Greenhouse, Lever, Workday, Ashby, iCIMS, SmartRecruiters, Workable, BambooHR and more — at no extra request, alongside company revenue, headcount and CEO. Use the Hiring companies dataset view for a company-level table.

(On the HTML engine the same field costs one extra request per job and must be enabled with resolveApplyUrls. On the default engine it is always there.)

Two engines

GraphQL (default). Indeed's mobile API. One request returns up to 100 jobs with full descriptions, GPS, employer firmographics and the resolved apply URL, and pages by cursor with no sign-in wall. A 200-job run costs 2 requests.

HTML (fallback). Scrapes search pages. Used automatically if the API fails, and selectable with engine: "html". Logged-out Indeed serves it one page per search (~15-48 jobs), so it reaches volume through query fan-out rather than depth, and full descriptions cost one extra request per job.

The HTML engine is best-effort, not a guarantee: Indeed blocks its search pages far more aggressively than the API, and on a bad exit IP it returns 403 outright. When that happens the run fails with the reason instead of handing you an empty dataset. Treat GraphQL as the engine and HTML as insurance, not as an equal second path.

The engines do not return identical fields, and no row pretends otherwise — every job carries a source field naming the engine that produced it:

FieldGraphQLHTML
descriptionTextyesyes (1 extra request/job)
latitude / longitude / streetAddress / postalCodeyesno
companyIndustry / companySize / companyRevenue / companyCeoyesno
attributes (skills, licences, seniority)yespartial
occupations (readable labels)yescodes only
atsUrl / atsProvideryes, freeyes, +1 request (resolveApplyUrls)
isExpiredyes, realflag only
companyRating / companyReviewCountnoyes
benefits / isSponsored / normalizedTitlenoyes

If you need company ratings, run engine: "html". For everything else the default is better and far cheaper.

Honest limits — read this before you buy

  • Sponsored placement. Much of Indeed's first HTML page is paid placement. The HTML engine flags every job with isSponsored; set includeSponsored: false to drop them. GraphQL does not expose that flag, so on the default engine isSponsored is always false.
  • Firmographics are not universal. Indeed only holds them for employers with a company profile. Measured on a 200-job run: companySize 156/200, companyRevenue 142/200, companyCeo 111/200, companyIndustry 32/200. Absent means Indeed has no data, not an error.
  • streetAddress is partial (72/200) — coordinates are always present, street level is not.
  • Residential proxies are strongly recommended. Datacenter IPs get security-checked on the HTML routes.
  • This Actor reads a session key from Indeed's own web client. If Indeed changes that, the run falls back to the HTML engine and says so in the log rather than dying.

Reliability

In that run the 7 jobs whose description page was blocked were still emitted with their listing data and descriptionText: null — 39 fetched + 1 free pane + 7 degraded = 47 rows, nothing dropped.

A run that scrapes zero jobs fails loudly with the reason — every page blocked, or no matching jobs — instead of finishing green with an empty dataset. If a job's description page is blocked, the job is still emitted with its listing-level data and descriptionText: null, so a bad IP costs you a field rather than the row.

Every run writes a CAPABILITY_REPORT record to the key-value store: which engine ran, how many GraphQL pages and jobs it returned, whether it fell back to HTML, whether the API key was harvested or a fallback was used, and what was blocked. If a run disappoints you, that record says why.

Cost control

On the default GraphQL engine there is nothing to tune: descriptions, GPS, firmographics and the ATS URL all arrive in the same request, 100 jobs at a time. maxItems is the only lever.

These inputs apply to the HTML engine only, and the run warns you in the log if you set them while GraphQL is active:

  • scrapeFullDescription — off gives a listings-only crawl, one request per search page instead of one per job.
  • resolveApplyUrls — one extra request per job to resolve the ATS link.
  • fanOut, maxPagesPerSearch, sort, jobTypes, experienceLevels, includeSponsored — ways of working around the HTML engine's one-page-per-search wall, which GraphQL does not have.

Scraping publicly available job listings is generally legal, and this Actor collects only public, non-personal data — job postings and employer information, no candidate profiles or resumes. You are responsible for how you use the output, including under GDPR/CCPA where relevant. When in doubt, take legal advice.