# Google Jobs Scraper API (`shakamize/google-jobs-scraper`) Actor

Scrape Google's jobs results into structured postings: title, company, location, posting source, posted date, employment type, full description and every apply link. Priced per job, and never charged for a search that returns nothing.

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

## Pricing

from $1.67 / 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Jobs Scraper API

Turns a Google jobs search into structured job postings: title, company,
location, the board Google indexed it from, when it was posted, employment type,
salary and benefits where Google shows them, the full description, and **every
apply link** - typically four to eight per posting, as direct employer and board
URLs rather than Google redirects.

Google's jobs results are an aggregator. One search returns postings syndicated
from LinkedIn, Indeed, Glassdoor, ZipRecruiter, SimplyHired, company careers
sites and niche boards, already merged and de-duplicated by Google. That
cross-board feed is the thing worth scraping, and nothing else assembles it.

Priced **per job**, at $0.002. A search that returns nothing is never charged.

Use it to build or fill a job board, track which roles a company is hiring for,
watch a competitor's headcount grow, or collect postings and apply links for a
recruiting pipeline. Coverage is Google's aggregation across boards and employer
sites rather than one board's full inventory, so what you get follows what
Google indexes for your query; for a single board's complete catalogue, scrape
that board directly.

### How many Google jobs you can actually scrape per search, and why it is 10

Read this before comparing us to anything else on the store.

**Google renders exactly 10 jobs per request, and its "load more" control does
nothing for a non-browser client.** Measured live on 2026-08-29:

| What was tried                                       | Result                                  |
| ---------------------------------------------------- | --------------------------------------- |
| `&start=10`, `&start=20`, `&start=30`                | a 200KB page with no jobs widget at all |
| the infinite-scroll cursor on its own async endpoint | HTTP 200 and an empty control frame     |
| scrolling the page to the cursor in a real browser   | **no request fired**; still 10 jobs     |
| `&tbs=qdr:d`                                         | kills the jobs widget entirely          |

That ten-result ceiling is Google's, and it is why `orgupdate`, the top-ranked
actor in this niche, carries an open issue titled *"Only 10 results"*.

**More jobs come from Google's own filter links.** Set **Max jobs per search**
above 10 and the actor follows the filter links the page itself offers - remote,
full time, part time, contract, internship, posted in the last N days - and
removes the jobs that repeat. Measured: 9 requests returned **73 distinct jobs**
for one search. The filter links are read off the page, so they are always in
the market's own language, and only links that *narrow* your search are
followed; Google's "related searches" rewrite it, and are ignored.

Every extra request is logged, and when Google offers fewer filters than the
jobs you asked for, the log says so rather than quietly returning less.

### Which countries Google serves jobs results in

Page size gives it away before the parse does: a market with the widget returns
850KB to 1,050KB, one without returns 200KB to 400KB. Measured 2026-08-29:

| Market                                | Jobs results | Note                                         |
| ------------------------------------- | ------------ | -------------------------------------------- |
| United States, United Kingdom, Canada | yes          |                                              |
| India, Australia                      | yes          |                                              |
| Germany                               | yes          | only when the search is worded a certain way |
| France, **Spain**, **Italy**          | yes          | **only when queried in the local language**  |
| Netherlands, Ireland, Poland, Sweden  | no           | nothing in any phrasing tried                |

**Spain and Italy are widely documented as dead markets for Google jobs. They
are not.** They return nothing to an English query and ten jobs to one in
Spanish or Italian. Language, not country, is the trigger. This actor picks the
language that works for the market you choose, so you do not have to know that.

For the markets that return nothing, the actor **says so in the log and writes
an error record** naming the market, rather than finishing successfully with an
empty dataset that reads like a place with no openings.

### The wording of a search decides whether Google answers it

This is the single strangest thing about the surface, and it costs the
incumbents whole markets:

```
software engineer jobs in Berlin   ->  0 jobs, 209KB
software engineer jobs berlin      ->  10 jobs, 885KB
```

Same country, same language, same day. So a search here is a **ladder**: the
actor tries one phrasing, and if Google renders no jobs widget it re-words the
search and tries again. That second attempt is what makes Germany work at all,
and it costs an extra request only when the first phrasing fails.

### Location: you get the place you asked for

*"Location issue"* is the highest-subscribed complaint in this niche, so:

- **A location is required.** Google scopes its jobs results to a place, and a
  bare keyword usually returns nothing. The actor refuses the run and says why,
  rather than returning arbitrary results.
- **Postings from elsewhere are dropped.** Google's own "remote" filter link, on
  a Chicago search, returns postings in Japan, Dallas and Hartford. Those are not
  answers to the question you asked, so by default they do not reach your
  dataset - and the log names every one that was dropped. Set **Location match**
  to `off` to keep them.
- Verified: `nurse jobs in Austin, TX` returned 10 of 10 in Austin; Seattle,
  London, Madrid and Toronto likewise.

### What you get back for each job

One record per posting:

| Field                                  | What it is                                                              |
| -------------------------------------- | ----------------------------------------------------------------------- |
| `jobId`                                | Google's own identifier, stable across searches and runs                |
| `title`, `companyName`, `location`     | as Google writes them                                                   |
| `via`                                  | the board or careers site the posting was indexed from                  |
| `postedAt`, `postedAtText`             | an ISO date, plus Google's own wording in the market's language         |
| `employmentType`, `salary`, `benefits` | read from the labels beside the posting                                 |
| `chips`                                | every label, raw, so nothing is lost to classification                  |
| `highlights`                           | Google's own extraction: qualifications, responsibilities, benefits     |
| `description`                          | the full posting text, including what the page hides behind "Show more" |
| `descriptionIsTruncated`               | true when Google served a preview instead of the whole posting          |
| `applyLinks`                           | every route, as direct URLs with the board each belongs to              |
| `searchQuery`, `searchKeyword`, ...    | which search found it, including a re-worded one                        |

**`descriptionIsTruncated` is worth knowing about.** Google serves two layouts.
One carries the whole posting - about 4,500 characters in our measurements - and
the other only a short preview of about 400, and which one you get depends on
the market. Rather than present a preview as a full description, the actor
labels it. Follow an apply link for the rest.

### How duplicate job postings are removed

*"Duplicates"* is the second-loudest complaint in this niche. Every job carries
Google's own identifier, and repeats are removed **exactly** on that id, never by
fuzzy matching on title and company. This applies within a run automatically,
across every filter the fan-out follows. Turn on **Only jobs not seen before** to
also skip jobs stored by previous runs, remembered in a named key-value store so
separate saved searches keep separate memories.

### How to filter Google Jobs by company, employment type, date and remote

Every filter runs over what was parsed, never over the query sent to Google.
Google's filters are localised free text with no stable identifier, so asking
Google to filter would mean guessing a phrase and silently returning the wrong
jobs when the guess is wrong.

- **Employment type** - full-time, part-time, contractor, internship and so on
- **Remote only**
- **Posted within N days** - jobs Google gives no age for are dropped, never
  assumed recent
- **Only these companies** / **Exclude companies** - useful against staffing agencies
- **Only these sources** - for example only LinkedIn, or only employer sites

Setting any filter makes the actor spend extra requests, so the filters have
material to work on.

### Pricing: $0.002 per job, and nothing for an empty search

One event, one job. A run costs jobs x $0.002, which is arithmetic you can do
before you start it.

*"Charged $1.21 but returned zero results"* is on the usage leader's issue
tracker. Here, a job is charged **after** its record is stored, so a search that
returns nothing costs nothing, and the dataset says so:

```json
{
  "error": "no_jobs_found",
  "errorDescription": "Google rendered no jobs for \"welder jobs in Nuuk\" or \"welder jobs Nuuk\" in US. Nothing was charged.",
  "input": "welder / Nuuk"
}
```

Every search leaves a record - a job or an explanation - so a partial run can
always be reconciled against what you asked for, and an empty market is never
confused with a page that could not be read.

### Proxies for scraping Google Jobs without getting blocked

Google's jobs results are fetched through Apify's unblocking service, which is
configured by default and needs nothing from you. A plain datacenter or
residential address is redirected away from the jobs results, so changing this is
rarely useful.

### Errors: what happens when a Google Jobs search cannot be scraped

| Code             | Meaning                                                                  |
| ---------------- | ------------------------------------------------------------------------ |
| `no_jobs_found`  | Google rendered no jobs for either phrasing of this search. Not charged. |
| `request_failed` | The page could not be read after retries. Not charged.                   |

### FAQ

#### Why do I only get 10 results per search?

Google's jobs vertical serves ten results per request and publishes no working
cursor for a non-browser client. `&start=` offsets do nothing, the load-more
token does nothing for a non-browser client, and scrolling a real browser to the
load-more sentinel fires no network request at all.

More jobs come from Google's own filter links, which this actor follows. It also
states the true ceiling up front rather than letting you wonder:

```
WARN  40 more job(s) were asked for, and Google offers only 2 filter(s) on this
      search, so it can return at most 30. This is Google's ceiling, not a cap
      applied here.
```

#### Will I be charged if the search returns nothing?

No. Charging is per job posting stored, at $0.002 each. On a billed run with two
searches on 2026-09-01, one returning ten jobs and one returning none, the
charge was exactly `{"job":10}`. The empty search consumed requests on our side
and nothing on yours.

Jobs dropped by your location or company filters are not charged either. If it
is not in your dataset, it is not on your bill.

#### How do I cap what a run can cost?

`maxJobsPerQuery` caps each search, `maxJobs` caps the whole run. Because the
billed unit is a stored job, those two bound the bill absolutely. Ten jobs is
$0.02; a 19-job run measured on 2026-09-01 cost $0.038.

#### Why does my search return nothing when the same search works in my browser?

Whether Google renders the jobs widget depends on the wording and language of the
query, not the country. `... jobs in Berlin` returns nothing where `... jobs
Berlin` returns ten, and Spain and Italy return full results in Spanish and
Italian but nothing in English.

This actor retries an alternate phrasing automatically and logs it. If you are
judging whether a market has data at all, test more than one phrasing and the
local language first.

#### Why are some jobs missing a salary?

Google publishes pay on roughly 1 to 3 job cards in 10. Across a 19-job run on
2026-09-01, salary was present on **8 of 19**, employment type on 18, posted date
on 14, and source and benefits on all 19.

That is normal rather than a collapse, and our own coverage monitoring treats it
that way. A tool that reports a salary on every posting is inferring it.

#### Why did jobs get dropped for location?

Google returns postings from elsewhere under its own filters. On an Austin
search, the "remote" filter returned postings in Georgia, Kentucky and Colorado.
Those are dropped, the drop is reported with the places it dropped, and
`locationMatch: "off"` keeps them if you want them.

### Telemetry

The actor reports run health to its maintainer: error codes, the build number,
and per-field counts of how often a value was found.

When requests fail, it also sends a small, capped sample of the failures
themselves, so that a broken run can be diagnosed and fixed: the Google search
URL that was requested, the HTTP status code that came back, and the error with
its stack trace. **That URL contains the search you asked for - the keyword and
the location are part of it.** It carries no personal data, but it is your
input, and this section says so rather than leave you to find out.

Your proxy configuration and the scraped job listings themselves are never sent.
A run that succeeds sends the same summary without the failure sample: how many
results it stored, the run and build it came from, and the per-field counts.
Nothing about a healthy run is worth hiding, and a run nobody hears about
cannot be improved.

### Legal and privacy

Job postings are company data, and that is all this actor returns. It collects
**no personal data**: no recruiter names, no employer contact emails, no
applicant information. That is a deliberate limit rather than a missing feature -
enrichment of that kind is personal data of EU residents and carries GDPR
exposure regardless of whether anyone is signed in.

Only publicly visible search results are read, and nothing is signed into.

# Actor input Schema

## `searches` (type: `array`):

One entry per search, as <code>{ "keyword": "software engineer", "location": "New York, NY" }</code>. A location is required: Google scopes its jobs results to a place, and a bare keyword usually returns nothing at all.

## `keywords` (type: `array`):

Job titles or skills. Combined with every entry of <b>Locations</b>, so 3 keywords and 2 locations run 6 searches.

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

Cities, regions or countries, written as a job seeker would type them. Verified to return only jobs in the requested place: <code>nurse jobs in Austin, TX</code> returned 10 of 10 in Austin.

## `countryCode` (type: `string`):

The Google country to search from. Measured 2026-08-29: the United States, United Kingdom, Canada, India, Germany, France, Spain and Italy all return jobs. The Netherlands, Ireland, Poland, Sweden and Israel returned none in any phrasing, and a run against them says so in the log rather than looking like a market with no openings.

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

Google interface language, as a two-letter code. Defaults to the language that returns jobs for the chosen country. This matters more than the country does: Spain and Italy return nothing to an English query and full results to one in Spanish or Italian.

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

Google renders at most 10 jobs per request and its load-more control does nothing for a non-browser client. Above 10, the actor follows the filter links Google itself offers on the page, in the market's own language, and removes the jobs that repeat. Every extra request is logged.

## `maxJobs` (type: `integer`):

Stop the whole run after this many jobs, across every search. Prefilled with 100 so a run that fans out over many keywords and locations has a ceiling; clear it for no overall cap.

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

Store the complete job description Google holds, not just the highlights. On by default and free: it is on the same page either way.

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

Keep only jobs Google labels with one of these. Applied to the parsed label rather than to the query, so it never silently returns the wrong jobs when a market words the filter differently.

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

Keep only jobs whose location, title or labels mark them as remote or work from home.

## `postedWithinDays` (type: `integer`):

Keep only jobs posted in the last N days. Google publishes an age rather than a date, so this compares against the date implied by that age at the time of the run. Jobs Google gives no age for are dropped, never assumed recent.

## `companies` (type: `array`):

Keep only jobs whose company name contains one of these.

## `excludeCompanies` (type: `array`):

Drop jobs whose company name contains one of these. Useful for filtering out staffing agencies.

## `sources` (type: `array`):

Keep only jobs syndicated via one of these boards, matched against the <code>via</code> field, for example <code>LinkedIn</code> or <code>Indeed</code>.

## `locationMatch` (type: `string`):

Google's own filter links widen a search as well as narrowing it: following its "remote" link on a Chicago search returns postings in Japan and Dallas. On <b>strict</b>, the default, a posting whose location does not name the place you asked for is dropped and counted in the log. This also drops region-wide postings that name only the country or state, such as <code>España</code> for a Madrid search. Set it to <b>off</b> to keep everything Google returned.

## `onlyNewJobs` (type: `boolean`):

Remember the jobs stored by previous runs and skip them. Jobs repeated inside a single run are always removed, with or without this.

## `newJobsStoreName` (type: `string`):

The named key-value store holding the jobs already seen. Use a different name per saved search to keep their memories apart.

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

Defaults to Apify's unblocking service, which is what makes Google's jobs results reachable over plain HTTP. Changing this is rarely useful: a plain datacenter or residential address is redirected away from the jobs results.

## Actor input object example

```json
{
  "searches": [
    {
      "keyword": "software engineer",
      "location": "New York, NY"
    }
  ],
  "keywords": [
    "software engineer"
  ],
  "locations": [
    "New York, NY"
  ],
  "countryCode": "US",
  "language": "en",
  "maxJobsPerQuery": 10,
  "maxJobs": 100,
  "includeDescription": true,
  "remoteOnly": false,
  "locationMatch": "strict",
  "onlyNewJobs": false,
  "newJobsStoreName": "google-jobs-seen",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

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

Every job Google returned for the searches you asked for, deduplicated, with the direct apply links for each.

# 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 = {
    "searches": [
        {
            "keyword": "software engineer",
            "location": "New York, NY"
        }
    ],
    "keywords": [
        "software engineer"
    ],
    "locations": [
        "New York, NY"
    ],
    "maxJobs": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("shakamize/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 = {
    "searches": [{
            "keyword": "software engineer",
            "location": "New York, NY",
        }],
    "keywords": ["software engineer"],
    "locations": ["New York, NY"],
    "maxJobs": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("shakamize/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 '{
  "searches": [
    {
      "keyword": "software engineer",
      "location": "New York, NY"
    }
  ],
  "keywords": [
    "software engineer"
  ],
  "locations": [
    "New York, NY"
  ],
  "maxJobs": 100
}' |
apify call shakamize/google-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,shakamize/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/bRgWFSHLp6awdR8YH/builds/MmeUAVuu5Q4LbRCAf/openapi.json
