# Google Jobs Scraper with Salary and Apply Links (`zovo-one/google-jobs-scraper`) Actor

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.

- **URL**: https://apify.com/zovo-one/google-jobs-scraper.md
- **Developed by:** [Michael Lip](https://apify.com/zovo-one) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 66.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 1,000 job listings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Jobs Scraper with Salary and Apply Links

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

```bash
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`:

```python
import os
from apify_client import ApifyClient

client = 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](https://theluckystrike.github.io/zovo-plugin-factory/apify/google-jobs-scraper/).

### What you get for each job

| Field | Example |
|---|---|
| title | Senior Software Engineer |
| company | Acme Corp |
| location | Austin, TX |
| isRemote | false |
| via | LinkedIn |
| 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.

```json
{
  "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 `isRemote` field 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

```json
{
  "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. `matchedSearch` says which search found each job, and `page` counts 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 `descriptionTruncated` is 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.

[zovo-support-start]: #

### Support Zovo

Zovo builds and maintains this Actor. If it saves you time, you can support the work on [GitHub Sponsors](https://github.com/sponsors/theluckystrike). Sponsorship is optional, and the tool works the same without it.

You can also use [PayPal](https://www.paypal.com/paypalme/najzzz).

Zovo is made by [theluckystrike](https://github.com/theluckystrike) on GitHub.

#### Need your format supported?

Open a [format request](https://github.com/theluckystrike/zovo-plugin-factory/issues/new?template=format-request.yml) with a sample file, and I reply there whether it can be done. For USD 299 once, through [this GitHub Sponsors tier](https://github.com/sponsors/theluckystrike/sponsorships?tier_id=679995), 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](https://apify.com/zovo-one/app-store-google-play-reviews). Gets App Store and Google Play reviews in one run, with one schema.
- [Website Screenshot API](https://apify.com/zovo-one/website-screenshot-pdf-api). Saves full-page screenshots and PDFs of public web pages.
- [YouTube Transcript Scraper](https://apify.com/zovo-one/tubetext-youtube-transcript-scraper). Gets YouTube transcripts and captions from videos, playlists and channels.
- [Google Trends Scraper](https://apify.com/zovo-one/google-trends-scraper-fast). 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](https://marketplace.visualstudio.com/items?itemName=zovo.zovo-json-tree-viewer). 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](https://marketplace.visualstudio.com/items?itemName=zovo.table-viewer-vscode). Opens CSV, TSV, Excel and Parquet files as a sortable, filterable table.
- [JSONL Viewer, Large JSONL Table & NDJSON Viewer](https://marketplace.visualstudio.com/items?itemName=zovo.zovo-jsonl-viewer). Opens JSONL and NDJSON files as a table, as records or as a chat view.

[zovo-support-end]: #

# Changelog

This Actor's version history is a separate document: https://apify.com/zovo-one/google-jobs-scraper/changelog.md

# Actor input Schema

## `queries` (type: `array`):

One search per line, up to 50, for example: software engineer, registered nurse, barista. Each line runs as its own Google Jobs search. If the list is empty, the Actor runs the example search "software engineer".

## `location` (type: `string`):

City, region or country, for example "Austin, TX" or "Berlin, Germany". Added to every search as "near <location>". Leave empty for no location. Up to 100 characters.

## `datePosted` (type: `string`):

Adds Google's own date phrase to the search ("since yesterday", "in the last 3 days", ...). Rows whose English posted age is older than the window are also dropped and not charged. Ages Google shows in other languages are not checked.

## `employmentType` (type: `string`):

Adds the job type to the search. A row that shows a different job type is dropped and not charged. Rows that show no job type are kept.

## `remoteOnly` (type: `boolean`):

Adds "remote" to the search. Google decides what matches; the isRemote field shows what the listing itself says.

## `maxJobsPerQuery` (type: `integer`):

Stop each search after this many saved jobs (1-500). Google shows about 10 jobs per page, so 20 jobs is about 2 pages. Paging stops early when Google has no more new jobs, so a search can return fewer than this.

## `country` (type: `string`):

Google's country setting (gl). It shapes results; use Location to pick the place.

## `language` (type: `string`):

Google's interface language (hl). Text such as "3 days ago" comes in this language.

## `dedupe` (type: `boolean`):

Skip a job already saved in this run by another search. Jobs with a Google job ID match on that ID only. Jobs without one match on the apply URL, and on title, company and location when they have neither. Skipped duplicates are not charged. Repeats inside one search are always skipped. After a platform restart, jobs a search already saved are skipped either way.

## `source` (type: `string`):

"google" reads the Google Jobs page through the proxy below. "serpapi" calls SerpApi's google_jobs API with your own key; SerpApi bills you for each search separately.

## `serpApiKey` (type: `string`):

Only for the SerpApi source. Your key from serpapi.com/manage-api-key. It is stored encrypted and never written to the log or OUTPUT.

## `proxyConfiguration` (type: `object`):

Google source only. Google answers plain connections with a "turn on JavaScript" page instead of jobs, so the default is Apify Proxy group GOOGLE_SERP, which Apify runs for Google Search pages. A page like that is fetched again through the proxy (up to 4 tries) and never charged. Other groups or your own proxies are allowed.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "datePosted": "any",
  "employmentType": "any",
  "remoteOnly": false,
  "maxJobsPerQuery": 20,
  "country": "us",
  "language": "en",
  "dedupe": true,
  "source": "google",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "GOOGLE_SERP"
    ]
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

No description

## `allFields` (type: `string`):

No description

## `runReport` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "software engineer"
    ],
    "datePosted": "any",
    "employmentType": "any",
    "remoteOnly": false,
    "maxJobsPerQuery": 20,
    "country": "us",
    "language": "en",
    "dedupe": true,
    "source": "google",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "GOOGLE_SERP"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zovo-one/google-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["software engineer"],
    "datePosted": "any",
    "employmentType": "any",
    "remoteOnly": False,
    "maxJobsPerQuery": 20,
    "country": "us",
    "language": "en",
    "dedupe": True,
    "source": "google",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["GOOGLE_SERP"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("zovo-one/google-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "software engineer"
  ],
  "datePosted": "any",
  "employmentType": "any",
  "remoteOnly": false,
  "maxJobsPerQuery": 20,
  "country": "us",
  "language": "en",
  "dedupe": true,
  "source": "google",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "GOOGLE_SERP"
    ]
  }
}' |
apify call zovo-one/google-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zovo-one/google-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RFInTaOrcT8SgmkHO/builds/WNnvGJL4NOxc7nvBy/openapi.json
