Google Jobs Scraper with Salary and Apply Links
Pricing
$4.00 / 1,000 job listings
Google Jobs Scraper with Salary and Apply Links
Google Jobs listings by search, location, date posted and job type. Title, company, salary, direct apply links, job highlights, posted date and full description. Blocked pages are retried and never charged. $0.004 per job ($4 per 1,000). Call it from curl or Python.
Pricing
$4.00 / 1,000 job listings
Rating
0.0
(0)
Developer
Michael Lip
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Features
Share
Pull job listings from the jobs panel in Google search. You give it a search like "registered nurse", a location, how recent the posts should be and the job type. You get back one row per job with the title, company, location, salary when the listing shows one, every apply link (straight to the employer or job board, not through Google), the posted date, the full description and Google's job highlights.
You pay $0.004 for each job saved, so 1,000 jobs cost $4. Pages that Google blocks, duplicates and jobs outside your filters aren't charged.
Use it from code
Run the Actor and get the rows back in one HTTP call. Put your Apify API token (Console, Settings, API & Integrations) in APIFY_TOKEN.
curl -X POST "https://api.apify.com/v2/acts/zovo-one~google-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"queries": ["data analyst"], "location": "Chicago, IL", "datePosted": "3days", "maxJobsPerQuery": 30}'
The same run from Python, after pip install apify-client:
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("zovo-one/google-jobs-scraper").call(run_input={"queries": ["data analyst"],"location": "Chicago, IL","datePosted": "3days","maxJobsPerQuery": 30,})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item.get("title"), item.get("company"), item.get("salary"), item.get("applyUrl"))
The sync call waits up to 5 minutes. For bigger jobs, start the run with POST /v2/acts/zovo-one~google-jobs-scraper/runs and read the dataset when it finishes.
Price examples, a sample output row, the limits and common questions are also on the Zovo page for this Actor.
What you get for each job
| Field | Example |
|---|---|
| title | Senior Software Engineer |
| company | Acme Corp |
| location | Austin, TX |
| isRemote | false |
| via | |
| employmentType | Full-time |
| salary | $120K–$150K a year |
| salaryMin / salaryMax / salaryPeriod / salaryCurrency | 120000 / 150000 / year / $ |
| postedAgo / postedDaysAgo / postedDateApprox | 3 days ago / 3 / 2026-11-02 |
| applyUrl | https://careers.acme.example/jobs/123 |
| applyLinks | every apply link Google lists, each with its source name |
| description | the full job text, up to 20,000 characters |
| highlights | Google's Qualifications, Benefits and Responsibilities lists |
| jobId | Google's ID for the listing, handy for tracking a job across runs |
| googleJobUrl | the job on Google Jobs |
| searchQuery, matchedSearch, position, page, googleJobsSearchUrl, scrapedAt | where the row came from |
Apply links are the posting's own URLs, read from the page, with Google's utm_ tracking tags removed. When a page doesn't carry the direct URL for a button, that link is Google's redirect link for it, which still opens the posting.
Salary fields stay null when the listing doesn't show a salary. We never guess one. The currency is the symbol Google shows, so a "$" in Canada stays "$" and isn't turned into USD. A bound the listing doesn't give stays null too. "$100K+ a year" and "From $50,000 a year" have salaryMax null, and "Up to $40 an hour" has salaryMin null.
Quick start
Leave the input empty and run it. You'll get up to 20 "software engineer" jobs from Google in the United States. A typical input looks like this.
{"queries": ["registered nurse", "physical therapist"],"location": "Denver, CO","datePosted": "week","employmentType": "FULLTIME","maxJobsPerQuery": 50}
Each line in queries is its own search. You can run up to 50 searches in one run and ask for up to 500 jobs per search.
How the filters work
Google Jobs reads its filters from the search text, the same way its own filter buttons do. So the Actor adds plain phrases to your search, like "full time", "remote", "near Denver, CO" and "in the last week". The search above goes to Google as registered nurse jobs full time near Denver, CO in the last week. Each search's googleJobsSearchUrl opens that same search in your browser.
Then it checks each job again before saving it.
- Date posted. A job whose age ("5 days ago", "vor 5 Tagen", "il y a 5 jours") is older than your window is dropped and not charged. Ages are read in English, German, French, Spanish, Portuguese, Italian and Dutch. In other languages you rely on Google's own filter.
- Job type. A job that shows a different type is dropped and not charged. A job that shows no type at all is kept, since Google already matched it to your search.
- Remote. This only adds "remote" to the search. The
isRemotefield tells you what the listing itself says.
postedDateApprox is the scrape date minus the age Google shows, so "3 days ago" becomes a date three days back. It's a day-level estimate, and it's null for "30+ days ago".
Pricing
You pay $0.004 for each job saved to the dataset, which is $4 per 1,000 jobs. That job event is the only charge the Actor makes itself. The Pricing tab in Apify shows any run start fee.
These aren't charged.
- Pages where Google showed a JavaScript, captcha or script-only page instead of jobs
- Jobs outside your date or job type filter
- Duplicates, inside one search or across searches
- Searches that fail
Set a maximum charge for the run in Apify and the Actor stops right before it. It saves only the jobs that fit, and the run report says why it stopped.
When Google blocks a page
Google answers plain connections with a page that asks you to turn on JavaScript. That's why the default proxy is Apify Proxy with the GOOGLE_SERP group, which Apify runs for Google Search pages.
Through that proxy, Google sends one of two pages. One holds the jobs. The other is a script-only page that says there are no "Jobs" matches, even for a search like "software engineer". In our checks on 2026-11-09, between a quarter and three quarters of the answers were the script-only page, and the share changed by the hour. The Actor tells the two apart: a real search without jobs still carries Google's jobs viewer, and the script-only page doesn't. So it fetches the script-only page again, up to 10 times per page, and a real "no jobs" answer ends the search at once. If 30 pages in a row come back script-only, the run stops and fails with a message, so a bad hour can't run up a long, empty run.
If Google sends a JavaScript page, a captcha or HTTP 429, the Actor fetches the page again through the proxy, up to 4 tries per page. None of these pages are charged. Short proxy failures are retried too. That covers Apify Proxy's upstream errors (HTTP 590, 592 to 596 and 599) and a page that comes back empty or doesn't look like a Google page at all, which is fetched one more time. (GOOGLE_SERP doesn't use proxy sessions, so each try is simply a new request through it.) When every try fails, the run fails with a message that says so, instead of finishing "successfully" with an empty dataset.
SerpApi as a second source
If you already pay for SerpApi, set source to serpapi and paste your key into serpApiKey. The Actor then calls SerpApi's google_jobs API instead of Google and returns the same row format, with source set to serpapi. SerpApi bills you for each search on your own plan, and this Actor still charges $0.004 per saved job. When SerpApi says every search has no results, the run ends normally with an empty dataset instead of failing. Your key is stored as a secret input and is never written to the log or the run report.
Output example
{"searchQuery": "software engineer","searchLocation": "Austin, TX","jobId": "eyJqb2IiOiIxIn0=","title": "Senior Software Engineer","company": "Acme Corp","location": "Austin, TX","isRemote": false,"via": "Acme Careers","employmentType": "Full-time","salary": "$120K–$150K a year","salaryMin": 120000,"salaryMax": 150000,"salaryPeriod": "year","salaryCurrency": "$","postedAgo": "3 days ago","postedDaysAgo": 3,"postedDateApprox": "2026-11-02","applyUrl": "https://careers.acme.example/jobs/123","applyLinks": [{ "title": "Acme Careers", "url": "https://careers.acme.example/jobs/123" },{ "title": "LinkedIn", "url": "https://www.linkedin.com/jobs/view/999" }],"description": "We build things...","descriptionTruncated": false,"highlights": [{ "title": "Qualifications", "items": ["5+ years of experience with Python", "Experience with AWS"] },{ "title": "Benefits", "items": ["Health insurance", "401(k) matching"] }],"googleJobUrl": "https://www.google.com/search?q=software+engineer+jobs&ibp=htl%3Bjobs&htidocid=eyJqb2IiOiIxIn0%3D&hl=en-US","position": 1,"page": 1,"matchedSearch": "software engineer jobs near Austin, TX","source": "google","googleJobsSearchUrl": "https://www.google.com/search?q=software+engineer+jobs+near+Austin%2C+TX&udm=8&hl=en&gl=us","scrapedAt": "2026-11-05T12:00:00.000Z"}
The dataset has two ready-made views. Jobs is the main table, and Salaries shows the parsed salary columns.
Run report
Every run writes an OUTPUT record to its key-value store. When the input is invalid, or the proxy can't be set up, the run fails before searching and OUTPUT holds only the error. Otherwise it lists each search with its status (ok, no_jobs, partial, blocked or error), the pages fetched and the jobs found, saved, filtered out and skipped as duplicates. It also has run totals, the number of blocked and script-only pages and the reason the run stopped early, if it did.
Duplicates
With dedupe on (the default), a job already saved by another search in the same run is skipped. A job with a Google job ID matches on that ID. It also matches a job with another ID when both have the same title, company and location and the same apply URL for one posting (a URL with a number in it, like /jobs/77). So one posting that Google lists under a new ID in another search is still caught. Separate openings that share a general apply page without a number, like a small employer's /careers page, all stay. When SerpApi's job_id holds a posting ID (htidocid), the match uses that posting ID, so the search details also packed into job_id don't count. A job without an ID matches on its apply URL (ignoring utm_ tracking tags), and on title, company and location when it has no apply URL either. Repeats inside one search are always skipped.
If Apify restarts a run partway, each search skips the jobs it had already saved, and they aren't charged again. That holds with dedupe on or off.
Limits
- Google shows 10 jobs per search, and the proxy can't load its "more jobs" scroll. So for more than 10 jobs, the Actor runs the same search again with the date and job type refinements Google offers as filter buttons ("in the last 3 days", "part time") and keeps only jobs it hasn't seen.
matchedSearchsays which search found each job, andpagecounts the searches (1 is the plain one). A filter you set yourself stays fixed in every search. A query stops after 3 refined searches in a row bring nothing new, or after about 2 searches per 10 jobs you asked for (25 at most). In our tests, 20 jobs took 2 searches and 50 jobs took 6. Expect roughly 100 to 150 jobs at most for a broad search. 500 is a ceiling you set, not a promise. - Salary is read when the whole line is a pay amount with a period, in English, German, French, Spanish, Portuguese, Italian or Dutch ("$25 an hour", "45.000 € pro Jahr", "45 000 € par an"). The currencies read are the symbols $ € £ ₹ ¥ ₱ ₦, the prefixes US$ CA$ C$ AU$ A$ NZ$ HK$ S$ MX$ R$ R KSh RM Rp, and the codes USD EUR GBP INR CAD AUD NZD SGD HKD ZAR PHP NGN KES AED SAR MYR IDR MXN BRL CHF. Other languages, currencies and formats leave all salary fields null, and so do lines with more text after the period, such as "$1,000 a month bonus". A number per hour with no currency counts only as a range of 5 or more, like "15–20 an hour", so "2 per hour" isn't read as pay.
- Each page gets about 150 seconds across its tries, so a slow or failing proxy can't stall a run.
- Up to 50 searches per run, 200 characters per search and 100 characters for the location. Descriptions longer than 20,000 characters are cut, and
descriptionTruncatedis set to true. It's also true when Google shows only a short preview of the description, which happens in some interface languages (German, for one, shows about 400 characters). - The Actor reads the job data Google puts in its own page (the server-rendered layout Google uses since November 2026, and the older inline data before it). If Google changes that page, a run can find no jobs. It then fails with a message that says no jobs were found, instead of saving wrong rows, and you can report it in Issues. A search that really has no results ends the same way, after one request, or at most two when the page didn't look like a Google results page.
Data and terms
The Actor reads public Google search pages, with no login. Job listings are business data. The Actor doesn't collect recruiter names, emails or profiles on purpose, though a description can mention a contact the employer chose to publish. You're responsible for using the data in line with the laws that apply to you and Google's terms.
Support
Found a search that returns nothing, or a field that looks wrong? Open an issue on the Actor's Issues tab and include the run ID, so we can see what Google sent.
Support Zovo
Zovo builds and maintains this Actor. If it saves you time, you can support the work on GitHub Sponsors. Sponsorship is optional, and the tool works the same without it.
You can also use PayPal.
Zovo is made by theluckystrike on GitHub.
Need your format supported?
Open a format request with a sample file, and I reply there whether it can be done. For USD 299 once, through this GitHub Sponsors tier, I add your file format or a missing feature to a Zovo VS Code viewer, with tests, within 7 days.
More tools by Zovo
- App Store and Google Play Reviews. Gets App Store and Google Play reviews in one run, with one schema.
- Website Screenshot API. Saves full-page screenshots and PDFs of public web pages.
- YouTube Transcript Scraper. Gets YouTube transcripts and captions from videos, playlists and channels.
- Google Trends Scraper. Gets Google Trends interest over time, by region and related queries, with no browser.
Free VS Code extensions by Zovo for the files you export:
- Large JSON Viewer, JSON Browser & JSON Tree. Shows large JSON files as a searchable tree, copies JSONPath and turns arrays of objects into a table.
- XLSX Viewer & CSV Preview for Excel, XLS & Parquet. Opens CSV, TSV, Excel and Parquet files as a sortable, filterable table.
- JSONL Viewer, Large JSONL Table & NDJSON Viewer. Opens JSONL and NDJSON files as a table, as records or as a chat view.