# Google Jobs Scraper - Aggregated Postings + Apply Links (`lanternlab318/google-jobs`) Actor

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

## Pricing

$2.49 / 1,000 job postings

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

Give it job search terms and a place, and get Google Jobs' public postings back: the title, the company, the location, the site Google aggregated the posting from, the posting date, the salary and employment type Google shows, the description from its detail panel, and every apply link. No Google login, no cookies, no account of your own. **$2.49 per 1,000 job postings, no start fee.** See [Pricing](#pricing).

Input:

```json
{
  "queries": ["software engineer", "data analyst"],
  "location": "New York, NY",
  "country": "us",
  "language": "en",
  "maxJobs": 10
}
```

Output — one row per posting (`recordType: "job"`); per-term status rows and the run summary are free. Sample from a real run; Google's opaque identifiers and the description shortened:

```json
{
  "recordType": "job",
  "jobId": "1X83OtFWneacwscFAAAAAA==",
  "title": "Software Engineer, Agentic",
  "companyName": "Uber",
  "location": "New York, NY",
  "via": "Built In NYC",
  "postedAtText": "3 days ago",
  "postedAt": "2026-10-21T13:04:11.000Z",
  "postedAtEstimated": true,
  "salaryText": "$171K–$190K a year",
  "jobType": "Full-time",
  "description": "About the Role. Agentic Pods is building a new model for how Engineering partners…",
  "descriptionTruncated": false,
  "applyLinks": [
    { "title": "Apply on Built In NYC", "url": "https://www.google.com/goto?url=CAESzAEB6zswFc7obAdOABwiv3E…" },
    { "title": "Apply on Indeed", "url": "https://www.google.com/goto?url=CAESvQEB6zswFTPynKCUVlncPtw…" }
  ],
  "url": "https://www.google.com/search?q=software+engineer+New+York%2C+NY&ibp=htl;jobs&hl=en&gl=us&htidocid=1X83OtFWneacwscFAAAAAA%3D%3D",
  "query": "software engineer",
  "searchLocation": "New York, NY",
  "rank": 1,
  "language": "en",
  "country": "us",
  "partial": false,
  "partialReason": null,
  "collectedAt": "2026-10-24T13:04:11.942Z"
}
```

Limits:

- **Public data only.** The postings Google shows on its own jobs panel; no login, no cookies, no candidate or recruiter data. Google serves **ten postings per search term** and offers no way to page past them, so coverage grows with more search terms and more locations, not with a higher `maxJobs`.
- **Every search term gets a status row saying what happened.** A term Google answered with nothing is an empty result; a term cut short by a limit, a refusal or an unrendered page is `partial` with the reason — the two are never confused.
- **Your limits stop collection and appear in the row**: `maxJobs`, the `maxUpstreamRequests` request ceiling and the maximum charge per run (see [Pricing](#pricing)).

### What it does

- Searches each term separately, in the place you give, and labels every row with the term that produced it, so one run can cover many roles and cities.
- Returns every apply link Google lists for a posting — often six or more sites — with Google's own label for each.
- Deduplicates across the whole run: a posting that matches several of your terms is written once and charged once.
- Keeps the source identifier (`jobId`), the platform's own posting date (`postedAtText` exactly as Google words it, plus `postedAt` as an ISO estimate) and our collection time (`collectedAt`) as separate fields.
- Reads the posted/salary/employment-type chips by their icon rather than by their English label, so a German or Russian search fills the same fields as an English one.
- Says what happened per search term. A term Google answered with nothing is recorded as an empty result; a term that was cut short by a limit, a refusal or a page Google would not render is marked `partial: true` with the reason.
- Caps itself: `maxUpstreamRequests` is a hard ceiling on requests to Google for the whole run, derived from your input when you leave it empty, so a run is never unbounded.
- Charges only posting rows. Empty results, per-term status rows and the run summary are free.

### What it does not do

- No paging past the ten postings Google serves per search term — there is no such page to fetch.
- No employer-site scraping: the apply links are Google's own redirects, and the description is the one Google shows on its panel.
- No candidate, recruiter or applicant data, and no contact details beyond what the employer published inside the posting text.
- No other job board scraped directly, no login-only listings, no scheduling or monitoring built in (use Apify schedules).

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `queries` | required | One job search term per line, for example `software engineer`. Each is searched separately. |
| `location` | empty | City, region or country, for example `New York, NY`. Google Jobs has no separate location field, so this is appended to each term — which is how its own location filter is set. |
| `country` | `us` | Two-letter country code of the Google edition to search (`gl`): `us`, `de`, `ru`, `gb`. |
| `language` | `en` | Interface language of the search (`hl`). With `en` the posting date is also returned as an ISO estimate. |
| `maxJobs` | `10` | Upper bound on rows per search term. Google serves ten per term, which is also the maximum here. |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to Google for the whole run. Leave empty and the Actor works one out from your terms. |
| `proxyConfiguration` | Apify Proxy, `GOOGLE_SERP` | The proxy the requests go through. Keep the default — see [Why the Google SERP proxy](#why-the-google-serp-proxy). |

### Output

One row per posting, `recordType: "job"`:

| Field | Meaning |
| --- | --- |
| `jobId` | Google's own identifier for the posting (`htidocid`). The dedupe key. |
| `title` | The job title as Google shows it. |
| `companyName` | The employer, as Google names it. |
| `location` | The location as Google shows it on the card. |
| `via` | The site Google aggregated the posting from, for example `Indeed`, `Stepstone`, `hh.ru`. |
| `postedAtText` | The posting date exactly as Google words it, for example `3 days ago`, `vor 7 Tagen`. |
| `postedAt` | The same date as an ISO 8601 instant, **converted from that wording, so an estimate to within the unit Google used**. `null` when the wording is in a language this Actor does not convert. Google publishes no exact timestamp. |
| `postedAtEstimated` | `true` whenever `postedAt` is set, as a reminder that it is derived, not published. |
| `salaryText` | The salary as Google shows it, with the currency symbol it spells out in the chip's label, or `null` when the posting has none. Never derived. |
| `jobType` | The employment type as Google shows it, for example `Full-time`, `Vollzeit`. |
| `description` | The description text from Google's detail panel. |
| `descriptionTruncated` | `true` when Google only previewed the description instead of showing all of it, which it does in some countries. The full text is then on the employer's site behind an apply link. |
| `applyLinks` | Every apply link Google lists: `title` is its label for the site, `url` is Google's apply redirect. |
| `url` | The Google Jobs page for this posting, openable in a browser. |
| `query`, `searchQuery`, `searchLocation` | The term you gave, what was asked of Google, and the location. |
| `language`, `country` | The edition that was searched. |
| `rank` | Position in that term's results, counting from 1. |
| `partial`, `partialReason` | Always `false` / `null` on a posting row: the row exists because the posting was collected. |
| `collectedAt` | Our own collection time, separate from the posting date. |

One row per search term, `recordType: "query_status"`, never charged:

| Field | Meaning |
| --- | --- |
| `jobsReturned` / `jobsCharged` | Rows this term wrote and charged. |
| `duplicatesSkipped` | Postings an earlier term in the run had already collected. |
| `jobsAvailable` | How many postings the page held, before your own limits. |
| `serpLimitReached` | `true` when Google's own ten-per-term ceiling, not your `maxJobs`, ended the list. |
| `malformedJobs` | Cards on the page that carried no Google identifier, so they were not written rather than written half-read. |
| `partial` / `partialReason` | `true` with `upstream_request_cap`, `charge_limit_reached`, `blocked`, `page_not_rendered`, `upstream_error` or `not_collected` when the term's results are incomplete. |
| `statusText` | The same thing in a sentence, so the dataset explains itself. |

The last row is `recordType: "RUN_SUMMARY"` — postings collected and charged, empty terms, partial terms by reason, terms that hit Google's ceiling, dates converted, descriptions previewed, requests sent, bytes moved, and why collection stopped. The same object is also in the run's key-value store under `RUN_SUMMARY`.

#### Partial is not empty

`jobsReturned: 0` with `partial: false` means Google has no posting for that term and place. `partial: true` means the term's results are incomplete and there may well be more. A run that collected nothing *and* had partial terms fails rather than reporting an empty success.

#### Why the Google SERP proxy

Google answers a request for its jobs panel from a datacenter or residential IP with a short page whose only content asks for JavaScript — no postings, no captcha, HTTP 200. Reading that as "no jobs" would be wrong, so this Actor detects it and reports `partialReason: "page_not_rendered"` instead. The Apify Google SERP proxy group returns the rendered page, which is why it is the default. Leave it on.

### Limits and boundaries

- Ten postings per search term is Google's ceiling, verified for `us`, `de` and `ru`: a request for the second page returns a rendered page with no postings at all, and filtering by posting date returns the same postings again. Use more terms and more locations instead.
- `postedAt` is converted from Google's relative wording and is therefore an estimate; `postedAtText` is the unaltered source value. English wording is converted; other languages keep `postedAt: null` rather than a guess.
- In some countries Google renders only a preview of the description. Those rows carry `descriptionTruncated: true`.
- The apply links are Google's own redirects, because Google does not render the employer's URL anywhere on the page.
- Public postings only. No login, no cookies, no account. Personal data is not collected; a contact address inside a posting is text the employer published.

### Pricing

Pay per event, no start fee:

| Event | Price | Charged for |
| --- | --- | --- |
| `result` | $0.00249 ($2.49 per 1,000) | Each job posting row written. Per-term status rows, empty results and the run summary are free. |

Set a maximum charge for the run and the Actor stops before going over it.

# Actor input Schema

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

One job search term per line, for example "software engineer" or "registered nurse". Every term is searched separately and its results are labelled with it. A posting that matches several of your terms is returned once and charged once. Google serves ten postings per search term, so more coverage comes from more terms, not from a higher limit.

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

City, region or country to search in, for example "New York, NY", "Berlin" or "Moscow". Google Jobs has no separate location field: this is appended to every search term, which is exactly how the location filter on the page is set. Leave it empty to let Google pick by country.

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

Two-letter country code of the Google edition to search (the gl parameter), for example "us", "de", "ru", "gb". It decides which country's job market the results come from.

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

Interface language of the search (the hl parameter), for example "en", "de", "ru". Pair it with a matching country. With "en" the posting date is also returned as an ISO timestamp; in other languages Google's wording is returned as written and postedAt is null rather than guessed.

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

Upper bound on the rows one search term may produce. Google's jobs panel serves ten postings per search term and offers no way to page past them, so ten is both the default and the maximum; the run reports per term when that ceiling, rather than your setting, ended the list.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Google, retries included. At the ceiling collection stops - the search terms not reached are written with partialReason "upstream_request_cap" and the run then ends normally. Leave empty and the Actor works a ceiling out from your search terms, so a run is never unbounded.

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

The proxy the requests to Google go through. Keep the default: Google answers a datacenter or residential IP with a page that only asks for JavaScript, while the Apify Google SERP proxy group returns the rendered jobs page. With any other group the run reports partialReason "page_not_rendered" rather than pretending there are no jobs.

## Actor input object example

```json
{
  "queries": [
    "software engineer"
  ],
  "location": "New York, NY",
  "country": "us",
  "language": "en",
  "maxJobs": 10,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "GOOGLE_SERP"
    ]
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (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"
    ],
    "location": "New York, NY"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/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"],
    "location": "New York, NY",
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/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"
  ],
  "location": "New York, NY"
}' |
apify call lanternlab318/google-jobs --silent --output-dataset

```

## MCP server setup

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