# Google Jobs Scraper: Salary, Apply Links, ATS Detection (`arcadium/google-jobs`) Actor

Google Jobs listings (title, company, salary, job type, description, every apply link) by keyword and location; $8.10 per 1,000 jobs. $3.15 on Business plans, 60-150 jobs per search, the company's ATS board detected. SerpApi input names accepted, free error rows.

- **URL**: https://apify.com/arcadium/google-jobs.md
- **Developed by:** [Arcadium Data](https://apify.com/arcadium) (community)
- **Categories:** Jobs, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.15 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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: Salary, Apply Links, ATS Detection

Give it job searches ("software engineer", "registered nurse") and places ("Seattle, WA", "Berlin"), and it returns one JSON row per Google Jobs result: title, company, location, posted date, salary (as shown and parsed), job type, full description and every apply link Google lists. $8.10 per 1,000 jobs on the Free plan, $3.15 on Business plans and above; you pay only for rows you get.

**Why this actor**

- The company's own ATS posting is picked as the best apply link, with the ATS and board detected.
- Goes past Google's 10 jobs per page to 60-150 jobs per search, with Google's own filters.
- SerpApi parameter names (`q`, `date_posted`, `employment_type`, `gl`, `hl`, `num_pages`) and these field names are also accepted: `query`, `location`, `jobType`, `maxResults`, `maxPagesPerQuery`, `remote`.
- No login; recruiter names, emails and phones are never output. Empty searches are free rows.

Built by Arcadium Data: public data, dated and agent-ready, never personal data.

### Use with AI agents

The actor takes plain JSON and returns one flat row per job, so an AI agent or MCP client (for example through [mcp.apify.com](https://mcp.apify.com)) can call `arcadium/google-jobs` directly. Input example for "Data analyst jobs in Austin posted this week":

```json
{ "queries": ["data analyst"], "locations": ["Austin, TX"], "datePosted": "week" }
```

"Remote part-time customer support jobs, up to 50, short descriptions for an LLM":

```json
{ "queries": ["customer support"], "remoteOnly": true, "employmentTypes": ["PARTTIME"], "maxResultsPerQuery": 50, "maxDescriptionChars": 600 }
```

"Nursing jobs in three German cities, in German, with the company's applicant tracking system detected":

```json
{ "queries": ["Pflegefachkraft"], "locations": ["Berlin", "Hamburg", "München"], "country": "DE", "language": "de", "jobsDbExport": true }
```

Read `title`, `companyName`, `location`, `postedAt`, `salary`, `employmentTypes` and `applyUrl`; `description` holds the job text. The same posting found by two searches comes once (`jobId`). 100 jobs cost $0.27. A search that finds nothing or cannot be read comes back as a free row with `rowType: "error"`, a `code` (`no_match`, `blocked`, `unavailable`, `invalid_input`) and a `message`, never as a failed run, so an agent can tell "no jobs" from "try again later".

### What you get

| Field | What it holds |
| --- | --- |
| `jobId` | Google's id of the posting, stable across searches and runs (use it to dedupe). |
| `title`, `companyName`, `location` | Job title, hiring organisation and place as Google shows them. |
| `via` | Site Google read the posting from (LinkedIn, Indeed, the company's career site, ...). |
| `postedAgo`, `postedAt` | Age as shown ("3 days ago") and the approximate date it gives (`2026-10-19`). |
| `salaryText`, `salary` | Salary as shown and parsed: `min`, `max`, `currency`, `period` (hour, year, ...). |
| `employmentType`, `employmentTypes` | Job type as shown and normalised (`FULLTIME`, `PARTTIME`, `CONTRACTOR`, `INTERN`, `TEMPORARY`). |
| `qualification`, `workFromHome` | Google's qualification chip ("No degree mentioned") and its work-from-home flag. |
| `description`, `descriptionComplete` | The job description, and whether it is the full text or Google's preview. |
| `applyUrl`, `applyOptions` | Best apply link (the company's own ATS posting when listed) and every apply option with site, URL and detected ATS. |
| `googleJobsUrl` | Link that opens the posting in Google Jobs. |
| `searchText`, `query`, `searchLocation`, `position` | The exact text sent to Google, your query and location, and the rank. |
| `jobsDb` | With `jobsDbExport`: ATS, board, ATS posting id, clean apply URLs, fingerprint (see below). |

Output example:

```json
{
  "rowType": "job",
  "platform": "google-jobs",
  "jobId": "d6Zuw_TVelot7iRuAAAAAA==",
  "title": "Software Development Engineer, Network Monitoring and Remediation (NMR)",
  "companyName": "Amazon Web Services, Inc.",
  "location": "Seattle, WA",
  "via": "Indeed",
  "postedAgo": "5 days ago",
  "postedAt": "2026-10-17",
  "salaryText": "$144K–$194K a year",
  "salary": { "min": 144000, "max": 194000, "currency": "USD", "period": "year", "source": "structured" },
  "employmentType": "Full-time",
  "employmentTypes": ["FULLTIME"],
  "qualification": null,
  "workFromHome": false,
  "description": "DESCRIPTION\n\nAmazon's network carries traffic for hundreds of millions of customers ...",
  "descriptionComplete": true,
  "applyUrl": "https://www.indeed.com/viewjob?jk=917ad7a1f42a4176&utm_campaign=google_jobs_apply&utm_source=google_jobs_apply&utm_medium=organic",
  "applyOptions": [
    { "source": "Indeed", "label": "Apply on Indeed", "url": "https://www.indeed.com/viewjob?jk=917ad7a1f42a4176&utm_campaign=google_jobs_apply&utm_source=google_jobs_apply&utm_medium=organic", "domain": "indeed.com", "ats": null },
    { "source": "LinkedIn", "label": "Apply on LinkedIn", "url": "https://www.linkedin.com/jobs/view/software-development-engineer-network-monitoring-and-remediation-nmr-at-amazon-web-services-aws-4480587462?utm_campaign=google_jobs_apply&utm_source=google_jobs_apply&utm_medium=organic", "domain": "linkedin.com", "ats": null }
  ],
  "googleJobsUrl": "https://www.google.com/search?ibp=htl;jobs&q=software+engineer+jobs+in+seattle&htidocid=d6Zuw_TVelot7iRuAAAAAA%3D%3D",
  "searchText": "software engineer in Seattle, WA",
  "query": "software engineer",
  "searchLocation": "Seattle, WA",
  "position": 2,
  "language": "en",
  "country": "US",
  "scrapedAt": "2026-10-22T08:00:00.000Z"
}
```

### Input

```json
{
  "queries": ["software engineer", "product manager"],
  "locations": ["Seattle, WA", "New York, NY"],
  "datePosted": "week",
  "employmentTypes": ["FULLTIME"],
  "maxResultsPerQuery": 30
}
```

| Field | What it does |
| --- | --- |
| `queries` | What to search, as typed into Google ("software engineer", "barista part time"). |
| `locations` | Places; each query runs once per location ("software engineer in Seattle, WA"). Empty = the whole market. |
| `datePosted` | `any`, `today` (since yesterday), `3days`, `week`, `month`. |
| `employmentTypes` | `FULLTIME`, `PARTTIME`, `CONTRACTOR`, `INTERN`; several = one search each, merged. |
| `remoteOnly` | Google's Remote filter. |
| `maxResultsPerQuery` | Jobs per query and location (default 10 = one Google page, up to 200). |
| `maxItems` | Stop after this many jobs in total. A cost cap. |
| `country`, `language` | Google market and interface language, e.g. `DE` + `de`, `IN` + `en`, `BR` + `pt-BR`. |
| `includeDescription`, `maxDescriptionChars` | Job text on or off, and an optional length cap. |
| `jobsDbExport` | Adds the `jobsDb` merge fields. |
| `includeErrorRows` | A free row for every search without jobs (on by default). |

SerpApi parameter names and these input field names are also accepted as they are (`query`, `q`, `location`, `date_posted`, `employment_type`, `jobType`, `gl`, `hl`, `maxResults`, `maxPagesPerQuery`, `num_pages`, `remote`).

#### More than 10 jobs per search

Google shows 10 jobs per search and loads the rest only inside its own page script. With `maxResultsPerQuery` above 10 the actor also runs the filter variants Google itself offers on the page (each job type, then narrower date windows, then both) and merges them without duplicates. A broad search ("software engineer in Seattle") reaches 60-150 jobs this way; a narrow one stops as soon as the variants stop adding new jobs. `searchText` on each row says which variant found it. You pay per job delivered, never per page.

#### Job-board merge fields (`jobsDbExport`)

Many Google Jobs postings link to the company's own applicant tracking system. With `jobsDbExport: true` each row gets a `jobsDb` object:

| Field | What it holds |
| --- | --- |
| `ats`, `boardToken`, `boardKey` | ATS (`greenhouse`, `lever`, `ashby`, `workable`, `smartrecruiters`, `recruitee`, `personio`, `teamtailor`, `workday`) and the company's board on it. |
| `atsJobId` | `<ats>___<posting id>`, the same id our [Career Site Jobs](https://apify.com/arcadium/career-site-jobs) scraper gives that posting, for joins. |
| `atsUrl`, `applyUrls` | The ATS posting URL and every apply URL, without tracking parameters. |
| `fingerprint` | sha1 of company, title and location (lower case), to match the same job across aggregators. |

### Use it from code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("arcadium/google-jobs").call(run_input={
    "queries": ["machine learning engineer"],
    "locations": ["London"],
    "country": "GB",
    "datePosted": "3days",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["rowType"] == "job":
        print(item["companyName"], "|", item["title"], "|", item["salaryText"], "|", item["applyUrl"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('arcadium/google-jobs').call({
  queries: ['electrician'],
  locations: ['Phoenix, AZ', 'Dallas, TX'],
  maxResultsPerQuery: 40,
  jobsDbExport: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.jobsDb?.ats).length, 'jobs link to a company ATS');
```

### Pricing

You pay per job delivered, at the price of your Apify plan. Error rows and repeats (the same job found by another search) are free.

| Apify plan | Per 1,000 jobs |
| --- | --- |
| Free | $8.10 |
| Starter (Bronze) | $4.05 |
| Scale (Silver) | $3.60 |
| Business (Gold) | $3.15 |
| Platinum | $3.15 |
| Diamond | $3.15 |

Plus a one-off $0.001 per GB of memory when a run starts. Descriptions, apply links, salary parsing and the job-board fields are included.

Set a maximum cost per run in the Apify run options and the actor stops before it goes over.

### Related actors

- [Career Site Jobs Scraper: 9 ATS incl. Workday, Personio](https://apify.com/arcadium/career-site-jobs)

### Use cases

- Job market and salary research: who is hiring for a role, where, at what pay, by job type.
- Recruiting, sourcing and sales: companies hiring for X (a buying signal), with links to their career sites.
- Job boards and aggregators: fresh postings with full text and every apply link, deduplicated by `jobId`.
- Joining with ATS data: `jobsDbExport` gives the company's Greenhouse, Lever, Workday ... board and posting id.
- AI agents: "open jobs for X in Y this week" as clean rows.

### Limitations

- Google shows 10 jobs per search. Past that, coverage comes from Google's own filter variants, so the total per search is what those variants list (typically 60-150 for a broad search), not every job Google has.
- Results, ranking and the location area are Google's. A location is sent the way you would type it ("in Seattle, WA"); a very small place may return jobs from the surrounding area.
- Some markets show only a preview of the description (about 400 characters, `descriptionComplete: false`); the full text is on the apply links.
- Salaries are what Google shows; many postings have none.
- Labels such as "3 days ago" follow `language`; `postedAt`, `employmentTypes` and `salary.period` are normalised for en, de, fr, es, it, pt and nl.

### Privacy

Public data only: Google Jobs results as an anonymous visitor sees them. No login, no account, no cookies. Company names are organisations; recruiter names, emails and phone numbers are never output: emails and phone numbers in descriptions are replaced with `[email]` and `[phone]`, and greetings that name a person are removed.

### FAQ

**Why do I get fewer jobs than maxResultsPerQuery?**
Google had no more for that search: its page and filter variants stopped adding new jobs. Broaden the query or the location, or add more locations.

**Why is a search an error row with code no_match?**
Google Jobs listed nothing for it ("It looks like there aren't any Jobs matches"). Check the spelling or broaden it. It is free.

**Can I search any country?**
Yes: set `country` (and `language` for local labels), e.g. `"country": "IN"` or `"country": "DE", "language": "de"`.

**Something looks wrong in the output. Who do I tell?**
Open an issue on the actor page with the run link. We read them all.

# Actor input Schema

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

What to search on Google Jobs, as you would type it, e.g. \["software engineer", "registered nurse", "barista part time"]. Each query runs once per location and gives up to maxResultsPerQuery jobs.

## `locations` (type: `array`):

Places to search in, e.g. \["Seattle, WA", "Austin, Texas", "Berlin"]. Each query runs once per location (sent to Google as "<query> in <location>"). Leave empty to let Google pick the area of the country you search in. A city, region or country name works.

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

Only jobs posted within this window, the same filter as Google's own Date posted chip.

## `employmentTypes` (type: `array`):

Only these job types, as Google's Job type chip. Several types run one search each and are merged. Leave empty for all types.

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

Adds Google's Remote filter (work from home jobs). The workFromHome field shows Google's own flag per job.

## `maxResultsPerQuery` (type: `integer`):

Jobs per query and location. Google shows 10 per search; above 10 the actor also runs Google's own filter variants (job types and date windows) and merges them without duplicates, so a broad search reaches about 60-150 jobs. Narrow searches stop earlier when Google has no more.

## `maxItems` (type: `integer`):

Hard cap on charged job rows for the whole run, across all queries and locations. Leave empty for no cap.

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

Two-letter country code of the Google Jobs market to search, e.g. US, GB, DE, IN, CA. Salaries with a bare $ are labelled with this country's dollar (CAD for CA).

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

Google interface language code, e.g. en, de, fr, es, pt-BR. Labels such as "3 days ago" and "Full-time" come in this language; normalised fields (postedAt, employmentTypes, salary) are filled for en, de, fr, es, it, pt and nl.

## `includeDescription` (type: `boolean`):

Adds the full job description text Google shows (emails and phone numbers replaced with \[email] and \[phone]). Off gives smaller rows at the same price.

## `maxDescriptionChars` (type: `integer`):

Cut descriptions to this many characters (0 = keep them whole). Useful for LLM pipelines with a token budget.

## `jobsDbExport` (type: `boolean`):

Adds a jobsDb object to each row: the company's applicant tracking system (Greenhouse, Lever, Ashby, Workday, ...) and board detected from the apply links, the posting id that ATS uses, apply URLs without tracking parameters, and a company+title+location fingerprint. Use it to merge Google Jobs rows with ATS data or deduplicate across sources.

## `includeErrorRows` (type: `boolean`):

Adds a free row (rowType: error) for each search that found nothing or could not be read, so no input disappears silently.

## `maxRetries` (type: `integer`):

Advanced: attempts per results page after a timeout or a temporary block.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "locations": [
    "Seattle, WA"
  ],
  "datePosted": "any",
  "maxResultsPerQuery": 10,
  "country": "US",
  "language": "en",
  "includeDescription": true,
  "maxDescriptionChars": 0,
  "jobsDbExport": false,
  "includeErrorRows": true,
  "maxRetries": 3
}
```

# Actor output Schema

## `results` (type: `string`):

All rows of this run in the default dataset.

## `view_overview` (type: `string`):

No description

## `view_apply` (type: `string`):

No description

## `view_descriptions` (type: `string`):

No description

## `view_errors` (type: `string`):

No description

## `record_stats` (type: `string`):

Google results pages read, filter variants used, retries, proxy requests and cost per job for this run.

# 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"
    ],
    "locations": [
        "Seattle, WA"
    ],
    "datePosted": "any",
    "remoteOnly": false,
    "maxResultsPerQuery": 10,
    "country": "US",
    "language": "en",
    "includeDescription": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("arcadium/google-jobs").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"],
    "locations": ["Seattle, WA"],
    "datePosted": "any",
    "remoteOnly": False,
    "maxResultsPerQuery": 10,
    "country": "US",
    "language": "en",
    "includeDescription": True,
}

# Run the Actor and wait for it to finish
run = client.actor("arcadium/google-jobs").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"
  ],
  "locations": [
    "Seattle, WA"
  ],
  "datePosted": "any",
  "remoteOnly": false,
  "maxResultsPerQuery": 10,
  "country": "US",
  "language": "en",
  "includeDescription": true
}' |
apify call arcadium/google-jobs --silent --output-dataset

```

## MCP server setup

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

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/hpi6wlmVPUvX9eVxg/builds/cfBedQnAj0qLFHSo3/openapi.json
