# Google Search Results Scraper — SERP API (`webdatatools/google-search-scraper`) Actor

Google Search Results Scraper returns organic titles, real destination URLs and snippets through the Apify GOOGLE_SERP proxy. One row per accepted SERP page; failures go to diagnostics. Verified zero-result pages remain billable pages.

- **URL**: https://apify.com/webdatatools/google-search-scraper.md
- **Developed by:** [Murat Uzun](https://apify.com/webdatatools) (community)
- **Categories:** SEO tools, Lead generation, AI
- **Features:** Limited permissions
- **Stats:** 44 total users, 17 monthly users, 92.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.50 / 1,000 results

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

### What is Google Search Results Scraper?

Google Search Results Scraper is an Apify Actor that fetches Google search result pages (SERPs) and returns **organic results with real, ready-to-click destination URLs** as one clean, structured row per page. It works through the Apify Proxy `GOOGLE_SERP` group — the proxy pool Google accepts requests through — asking for Google's `&gbv=1` ("basic HTML") rendering, whose links can be a decodable `/url?q=<url>` redirect or an opaque `/url?q=<token>` wrapper. The modern page can use opaque `/goto?url=<token>` click trackers. The Actor decodes clear destinations and follows Google's own redirect for opaque wrappers, so `url` is the full destination either way, never a domain guessed from the breadcrumb. Priced **per accepted SERP page, not per organic listing**. Filter by date range, site, file type or exact phrase, and page through up to 10 result pages per query.

**Pricing:** Google Search Results Scraper costs **$5.00 per 1,000 results** (pay per result, no subscription; Apify's free plan credit covers small runs).

### What data does Google Search Results Scraper extract?

Google Search Results Scraper extracts the organic result list for every query/page combination you give it:

| Field                                                                                          | Type              | Description                                                                                    |
| ---------------------------------------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------- |
| `query`, `page`, `countryCode`, `languageCode`, `device`                                       | string, number    | The request that produced this row                                                             |
| `searchUrl`                                                                                    | string            | The exact Google URL fetched through the proxy                                                 |
| `organicResults`, `organicCount`                                                               | array, number     | `{position, title, url, displayedUrl, snippet, date, sitelinks[]}` per organic listing         |
| `localResults` | array | Visible local business cards with name, Google link, rating, review count, price range, category, address and literal opening text; missing fields are null |
| `relatedSearches` | array | Deduplicated suggestion labels from Google's basic and modern related-search widgets, in page order; excludes navigation and pagination |
| `peopleAlsoAsk`, `ads`, `adCount`, `featuredSnippet`, `knowledgePanelTitle` | array/object/null | Parsed when present; availability depends on the Google layout returned — see the FAQ |
| `totalResultsText`, `htmlLength`                                                               | string, number    | Google's own results-count text (rarely present) and the raw HTML size fetched                 |
| `errorCode`, `error`, `scrapedAt`                                                                           | string            | Null on accepted result pages; failures are in diagnostics, plus the ISO timestamp                |

### Result quality and billing

Each SERP page uses one proxy attempt, with no automatic paid-proxy retry. That request can wait up to 60 seconds, shortened when the run deadline is closer. Google result-link redirects use at most six direct attempts per unique link, within the same run deadline; they do not fetch a new SERP page. Explicit Google refresh waits up to 30 seconds are honored before following the new link. Each distinct link waits independently, so another link can finish while a delayed refresh is pending. Request concurrency, retry budgets and the run deadline still apply. After two rejected pages in a run, the Actor stops starting queued pages and lets in-flight pages finish. Effective concurrency is capped at 3 (older inputs up to 10 are still accepted). Completed, accepted pages are kept. If Google was accessed but every page was rejected, the run succeeds with an explicit no-usable-rows message; success does not mean data was found. If no rows were saved and any source fetch failed, the run fails. Inspect `OUTPUT.costGuardTriggered`, `costSkippedPages`, `rejectedPages` and `serpRequestAttempts` before retrying skipped queries. These counters describe the current process attempt, not cumulative migration/restart totals. Defaults use 256 MB memory. This limits avoidable usage; unsuccessful requests and in-flight work can still have costs.

Only accepted SERP pages enter the default dataset. `resultStatus` distinguishes `results` (organic listings with non-empty titles and real destination URLs), `no_results` (an explicit Google no-match message), and `features_only` (useful SERP widgets without organic listings). A verified zero-result page is still a billable SERP page. A small or large unknown page, blocked page, failed fetch or page with missing organic destination URLs is stored as a unique `DIAGNOSTIC-*` record instead. `OUTPUT` lists confirmed results, diagnostics and partial/delivery state; inspect it as well as the run status.

Diagnostics do not generate the default-dataset result event. The start fee and applicable platform/proxy usage may still apply. Check the live **Pricing** tab for your plan. The initial result allowance caps how many source pages are attempted, in query/page order; rejected pages are not replaced with extra proxy requests. Reached but unusable pages remain diagnostics; a completed run with only those pages succeeds without result events. Invalid input, unavailable proxy access, processing/storage errors and unconfirmed dataset delivery fail the run. An empty query list never starts an example search. Counts apply to the current process attempt; migration/restart deduplication is not claimed.

### How to use Google Search Results Scraper

1. Enter your search terms into **Search queries**, e.g. `apify web scraping`. One row comes back per query per result page.
2. Set **Max pages per query** (up to 10) if you need more than the first ~10 results — Google returns about 10 per page.
3. Optionally narrow the search with **Date range**, **Only this site**, **File type** or **Exact phrase**.
4. Pick **Device** (desktop or mobile User-Agent), click **Start**, then export as JSON, CSV, Excel or HTML — every `organicResults[].url` is the full destination link, ready to open or crawl further (pages with missing organic destination URLs go to diagnostics instead of the paid dataset).

### Example input

```json
{
    "queries": ["apify web scraping", "best crm for startups"],
    "countryCode": "us",
    "languageCode": "en",
    "resultsPerPage": 10,
    "maxPagesPerQuery": 1,
    "device": "desktop"
}
```

### JavaScript, Python and CLI examples

These examples start a cloud run when executed. Use an authorized account with sufficient credit. Save this small input as `input.json` (matching the release smoke input):

```json
{"queries":["apify web scraping"],"maxPagesPerQuery":1,"resultsPerPage":10,"countryCode":"us","languageCode":"en","maxConcurrency":1,"includeRelatedSearches":true}
```

JavaScript, with `apify-client` v2 installed and `APIFY_TOKEN` supplied securely by your environment:

```javascript
import { readFileSync } from 'node:fs';
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const input = JSON.parse(readFileSync('input.json', 'utf8'));
const run = await client.actor('webdatatools/google-search-scraper').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

Python, using the documented `apify-client` v3.5 API and the same environment variable:

```python
import datetime
import decimal
import json
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
with open("input.json", encoding="utf-8") as source:
    run_input = json.load(source)
run = client.actor("webdatatools/google-search-scraper").call(
    run_input=run_input,
    run_timeout=datetime.timedelta(seconds=120),
    max_total_charge_usd=decimal.Decimal("0.05"),
)
if run is None:
    raise RuntimeError("No run returned; inspect the run in Apify Console")
items = client.dataset(run.default_dataset_id).list_items().items
print(items)
```

CLI, after authenticating your Apify CLI account:

```sh
apify actors call webdatatools/google-search-scraper --input-file input.json --timeout 120 --output-dataset
```

The CLI call above sets a timeout, not a charge limit. The documented `actors call` options do not include a charge-limit flag; use the Python or MCP run options below when an explicit event-charge cap is required. The 120-second timeout is a limit, not a completion guarantee.

A dataset can contain partial results. Check the run status, its `OUTPUT` key-value record and diagnostics before treating the request as complete. These examples are documentation, not evidence of a successful integration run. Official references checked 2026-11-12: [JavaScript client](https://docs.apify.com/api/client/js/docs), [Python ActorClient.call reference](https://docs.apify.com/api/client/python/reference/class/ActorClient), [CLI command reference](https://docs.apify.com/cli/docs/reference).

`displayedUrl` preserves a domain or URL breadcrumb present in the page. Rich cards sometimes show an age, comment count or view count instead; that presentation field is then `null`, while the separately resolved destination stays in `url`.

### Example output

Selected fields from replaying the saved `test/fixtures/serp-desktop.html` through the production parser (2026-11-12), not a new live run. The organic array is complete; `organicCount` matches its length. All six useful output keys required by the release smoke check are included. The separate `error` and `errorCode` fields are null on accepted rows; they are not required non-empty smoke fields.

```json
{
    "query": "apify web scraping",
    "page": 1,
    "searchUrl": "http://www.google.com/search?q=apify+web+scraping&hl=en&gl=us&num=10&gbv=1",
    "organicResults": [
        {
            "position": 1,
            "title": "Apify: The largest marketplace of trusted tools for AI",
            "url": "https://apify.com/",
            "displayedUrl": "apify.com",
            "snippet": "68,172 tools for your AI ... Apify is the largest marketplace of trusted tools for AI. Get real-time web data, track competitors, generate leads, monitor social ...",
            "date": null,
            "sitelinks": []
        },
        {
            "position": 2,
            "title": "Apify - GitHub",
            "url": "https://github.com/apify",
            "displayedUrl": "github.com › apify",
            "snippet": null,
            "date": null,
            "sitelinks": []
        },
        {
            "position": 3,
            "title": "How to scrape ANY website with Apify's Web Scraper API - YouTube",
            "url": "https://www.youtube.com/watch?v=5kcaHAuGxmY",
            "displayedUrl": "www.youtube.com › watch",
            "snippet": null,
            "date": null,
            "sitelinks": []
        },
        {
            "position": 4,
            "title": "Apify Website Scraper Makes Web Scraping Way Easier With AI",
            "url": "https://www.reddit.com/r/AISEOInsider/comments/1w6gpti/apify_website_scraper_makes_web_scraping_way/",
            "displayedUrl": "www.reddit.com › AISEOInsider › comments › apify_website_scraper_ma...",
            "snippet": null,
            "date": null,
            "sitelinks": []
        },
        {
            "position": 5,
            "title": "Apify Tutorial: How To Use Apify To Scrape Any Website - YouTube",
            "url": "https://www.youtube.com/watch?v=iFWRZ3U_P5k",
            "displayedUrl": "www.youtube.com › watch",
            "snippet": null,
            "date": null,
            "sitelinks": []
        },
        {
            "position": 6,
            "title": "Apify - YouTube",
            "url": "https://www.youtube.com/apify",
            "displayedUrl": "www.youtube.com › apify",
            "snippet": null,
            "date": null,
            "sitelinks": []
        }
    ],
    "organicCount": 6,
    "relatedSearches": [
        "Apify web scraping tutorial",
        "Apify web scraping github",
        "Apify login",
        "Apify API key",
        "What is Apify",
        "Apify Console",
        "Apify Web Scraper Chrome extension",
        "Apify documentation"
    ],
    "resultStatus": "results",
    "error": null,
    "errorCode": null
}
```

### Input parameters

| Parameter                | Type    | Default                  | Description                                               |
| ------------------------ | ------- | ------------------------ | --------------------------------------------------------- |
| `queries`                | array   | Required; no runtime fallback | Search queries, one or more result pages per query        |
| `countryCode`            | string  | `"us"`                   | 2-letter country code (`gl`)                              |
| `languageCode`           | string  | `"en"`                   | 2-letter language code (`hl`)                             |
| `resultsPerPage`         | integer | `10`                     | Sent as `num`; Google currently returns ~10 per page anyway |
| `maxPagesPerQuery`       | integer | `1`                      | Result pages fetched per query (1-10), ~10 results each   |
| `dateRange`              | string  | —                        | `hour`, `day`, `week`, `month` or `year` (`tbs=qdr:`)     |
| `site`                   | string  | —                        | Adds `site:` to every query, e.g. `reddit.com` or `gov`   |
| `fileType`               | string  | —                        | Adds `filetype:` to every query, e.g. `pdf`               |
| `exactPhrase`            | string  | —                        | Adds a quoted phrase every result must contain            |
| `device`                 | string  | `"desktop"`              | `desktop` or `mobile` User-Agent                          |
| `includePeopleAlsoAsk`   | boolean | `true`                   | Parse People Also Ask when Google renders it (modern page only, see FAQ) |
| `includeRelatedSearches` | boolean | `true`                   | Parse related searches when Google renders them           |
| `safeSearch`             | boolean | `false`                  | Request SafeSearch filtering                              |
| `maxConcurrency`         | integer | `3`                      | Effective parallel pages 1-3; legacy values up to 10 are capped at 3 |
| `debugHtml`              | boolean | `false`                  | Save every fetched Google page as `PAGE_<n>.html` in the key-value store (for bug reports) |

There is no proxy configuration input. SERP fetches use the Apify Proxy `GOOGLE_SERP` group; destination-link resolution uses bounded direct requests to Google. Consult the live Pricing tab for applicable event and usage charges.

### Search filters and more than 10 results per query

The filters are applied to every query in the run: `dateRange` sets Google's "Any time" filter (`tbs=qdr:h/d/w/m/y`), and `site`, `fileType` and `exactPhrase` add `site:`, `filetype:` and a quoted phrase to the query. The row's `searchUrl` shows exactly what was sent.

Measured on Apify on 9 October 2026:

- `climate` with `exactPhrase: "annual report"`, `site: "gov"`, `fileType: "pdf"` and `dateRange: "year"`: page 1 had 10 results, all on `.gov` sites, all dated between 21 October 2025 and 29 September 2026, with PDF page counts in the snippets.
- **Paging past the first 10:** `pizza delivery` with `maxPagesPerQuery: 2` gave 8 results on page 1 and 10 more on page 2, with no title repeated. Each page is one row and one billed SERP page.
- **`resultsPerPage` above 10 does not help:** with `resultsPerPage: 100` Google returned 9 results. Pages are therefore requested at `start=10, 20, …` whatever `resultsPerPage` says, so no results are skipped between pages; use `maxPagesPerQuery` for depth.

A page whose proxy request times out is saved to diagnostics, outside the result dataset. Successful pages are retained. A run with no saved rows and any failed source fetch fails; reached pages rejected for unusable content are reported separately.

### Pricing

Google Search Results Scraper uses a **result** event per accepted Google SERP page, rather than per organic listing. The Free-plan rate is **$0.005 per page ($5.00 per 1,000 accepted pages)**; the paid-plan rate is $0.0045 per page in the pricing checked on October 28, 2026. The actor-start event is $0.00005 per event, with one event per GB of memory and a minimum of one. The live **Pricing** tab is authoritative. Failed, blocked, unrecognised and missing-destination pages go to diagnostics without a result event. Explicitly verified zero-result pages remain billable. Therefore accepted result count can be lower than queries × pages. Set **Maximum cost per run** (`maxTotalChargeUsd`); the Actor caps source attempts at the initial result allowance and stops before its timeout where possible.

### Google Search Results Scraper vs. apify/google-search-scraper

Both Actors are pay-per-event and bill per Google results page; compare the live pricing and enabled add-ons for your plan. `apify/google-search-scraper` offers AI Overview capture and broader research features. This Actor returns resolved organic destination URLs and related-search suggestions from the same fetched HTML, without an additional source request for those suggestions. Suggestions are deduplicated in page order and exclude navigation and pagination. People Also Ask and ad availability still depend on Google's layout. No AI Overview capture or general superiority over the broader Actor is claimed.

### Using Google Search Results Scraper with AI agents and MCP

Connect your MCP client to `https://mcp.apify.com?tools=call-actor,get-actor-run,get-dataset-items,get-key-value-store-record` and authorize with Apify OAuth. The official MCP documentation excludes full-permission Actors; it does not state that pay-per-event pricing is a general MCP requirement. Payment-specific eligibility is separate. See the [official MCP setup and tool selection guide](https://docs.apify.com/mcp) (checked 2026-11-12).

Example agent request: “Use `call-actor` for webdatatools/google-search-scraper with the small `input.json` above and `callOptions: {"timeout": 120, "maxTotalChargeUsd": 0.05}`. Keep run options separate from Actor input. Use the returned run ID with `get-actor-run` to check completion, then retrieve rows with `get-dataset-items` using the returned dataset ID. Inspect `OUTPUT` through `get-key-value-store-record` for partial results.”

`call-actor` returns run/status/storage identifiers, not dataset rows. Its short wait may finish before the Actor does; follow the same run rather than starting another. This example starts a billable cloud run only when executed by an authorized client.

**Short timeouts save completed results where possible.** Accepted SERP pages are saved in task order as earlier pages finish or are skipped. When the run's `timeout` is close (the last 10 s, or 5 % of a longer run, at most 2 min) the Actor starts no new pages, saves everything it has and attempts to finish with a status message such as `Stopped early before the run timeout: 12 of 40 SERP page(s) done…`. Read the run's `statusMessage` to tell a complete result from a partial one.

**Sizing a run.** Runtime depends on Google response times and redirect waits; there is no pages-per-minute guarantee. A November 12, 2026 run with three US/English queries, one page each and concurrency 1 took 87.861 seconds on build 0.44.6; this single observation is not a runtime guarantee. Effective page concurrency stays capped at 3. Allow sufficient timeout and inspect partial results before retrying. `maxTotalChargeUsd` limits the result allowance; applicable start/platform usage can still apply.

**Deadline and storage limits.** `OUTPUT.deadlineSource` identifies `platform-env`, `sdk-run-options`, `sdk-run-unlimited` or `local-unbounded`; `timeoutAt` is the absolute deadline or null. If the platform deadline is missing, the Actor reads only its current cloud run through the SDK, using a 2-second lookup with no retries, and derives the deadline from the actual start and run timeout. It never substitutes the Actor's default timeout for a local or unlimited run. Unavailable or invalid cloud run metadata fails setup before source work.

Cloud storage uses the SDK billing-aware client with HTTP retries disabled and a 2-second request timeout. The final dataset flush and individual dataset/key-value writes wait at most 2 seconds each, shortened to leave 250 ms before a known deadline. An expired write is not started. A timed-out upload may have reached storage: it is marked unconfirmed, never retried by this Actor, and the run fails rather than claiming delivery. Failed OUTPUT persistence also fails the run; consult the log when OUTPUT could not be saved. These are local write-wait limits, not a new timeout for an unlimited run.

### Diagnostics and partial results

Accepted dataset rows have `errorCode: null` and `error: null`. The key-value store holds rejected pages in `DIAGNOSTIC-*` records with `errorCode` equal to the existing `diagnosticReason`. Existing non-empty `error` text is preserved; a missing, empty or whitespace-only error receives a human-readable explanation for the reason code. This additive field does not change billing or `resultStatus`. These records are not default-dataset result rows. `OUTPUT.rejectedPages` and `rejectionReasons` count rejected pages by code; `accessedPages` counts successful source responses, including empty bodies, and `fetchFailedPages` counts failed source fetches. `OUTPUT.firstDiagnosticKey` points to the first saved diagnostic; inspect `OUTPUT` counters and run status even when some rows were delivered.

| `errorCode` (= `diagnosticReason`) | Human-readable explanation and next check |
| --- | --- |
| `EMPTY_INPUT` | No usable queries after normalization; provide non-empty strings. No example search is substituted. |
| `PROXY_UNAVAILABLE` | GOOGLE_SERP proxy access could not be obtained; inspect `error`. No SERP result is written. |
| `DATASET_DELIVERY_UNCONFIRMED` | Dataset write could not be confirmed. The Actor fails and does not retry the upload; inspect existing data before rerunning. Diagnostic persistence is best-effort in this failure path. |
| `PROCESSING_FAILED` | Processing or diagnostic storage failed; further source work stops. Inspect `error` and `OUTPUT.operationalFailure`; the diagnostic itself may fail to persist. |
| `SERP_FAILED` | Parsed row has an `error`, including fetch, blocking or parsing failure. Use `OUTPUT.fetchFailedPages` and `accessedPages` to distinguish unreachable sources from reached but rejected content. |
| `INVALID_RESULT_SHAPE` | Organic array/count is inconsistent; inspect the diagnostic row. |
| `MISSING_ORGANIC_DESTINATION` | An organic result lacks a useful title or destination URL; the whole page is rejected. |
| `UNRECOGNISED_SERP` | No organic results and neither recognized `no_results` nor `features_only` status. |

### FAQ

**What am I charged for?** Each accepted SERP page produces one result event, regardless of the number of organic listings. A verified `no_results` page is also billable. Diagnostics do not produce a result event, but start and applicable platform/proxy usage may still cost money. Related searches are parsed from the same HTML without an additional SERP request. See the live Pricing tab for current charges.

**Which Google page does the Actor get, and how does `url` get the full link?** Every request asks for Google's `&gbv=1` ("basic HTML") page. Clear `/url?q=<url>` destinations are decoded. On October 28, 2026, a captured basic page also used opaque `CAES…` tokens in `/url?q=<token>`; these are resolved through Google's own redirect, like modern `/goto?url=<token>` links. Google often ignores that and serves its modern JavaScript page instead: on 9 October 2026, 11 of 16 test fetches came back modern, and which one arrives varies by query and over minutes, not reliably by User-Agent (Lynx, Opera Mini and IE6 User-Agents were tried). On the modern page every result link is `/goto?url=<token>`, an encrypted token with no destination in the page. For those the Actor uses a bounded sequence of direct requests to Google's own redirect and reads the destination from its answer. In a 9 October cloud check (3 queries, one page each: 2 basic pages, 1 modern page) all 25 organic results had their full URL, including all 7 from the modern page.

**What happens if a destination URL cannot be resolved?** The whole affected SERP page is saved as a `DIAGNOSTIC-*` record with its available data, without a default-dataset result event. A breadcrumb is never substituted for the destination URL. An HTTP200 response may instruct a timed refresh to a new Google tracking URL. The Actor waits the stated delay (up to 30 seconds) and follows that wrapper within the existing maximum of six direct attempts per distinct original link. Loops, ambiguous refreshes and unrecognized pages remain unresolved; JavaScript is not executed and the destination website is not fetched. Direct redirect lookups share a maximum of three concurrent requests across the Actor, independently of page-fetch concurrency. `OUTPUT` records their response-status counts without storing tracker tokens. Check `OUTPUT` and diagnostics if a run delivers fewer accepted pages than requested.

**When are related searches available?** Both captured basic and modern Google layouts contain related-search widgets. The Actor reads their suggestion labels, including when `q` is not the first URL parameter. Five captured-page regressions preserve all 40 labelled suggestions; this is a parser regression result, not a guarantee for every query. Google may omit the widget. `includeRelatedSearches: false` returns an empty array and does not fetch anything extra.

**Are video and discussion results included?** Ordinary organic listings can use Google's richer video/discussion card layout alongside standard text results. Recognized cards are returned in their original result order, with the destination link and available snippet. A saved live-page regression preserves all eight listings, including two rich cards previously missed. Dedicated video carousels and AI answer citations are not treated as ordinary organic listings.

**Why did some result descriptions used to be blank?** Google's basic HTML also uses nested description blocks that differ from its modern layout. Recognized blocks now preserve the visible description, skip rating-only rows and navigation links, and separate a displayed date when present. Missing descriptions stay null. This extraction uses the existing page response, with no additional SERP requests.

**Why can People Also Ask, ads and featured snippets be empty?** These features depend on Google's returned layout and query. Basic HTML captures have no People Also Ask box; the modern captures contain question text. A same-HTML comparison found 4 + 4 + 0 = 8/8 visible questions in three saved pages. A separate run returned no PAA questions in a different Google layout. This does not establish a parser defect or superiority over another Actor. Answers loaded only after a browser click are not fetched. Missing widgets remain empty; the Actor does not invent answers or make extra paid proxy requests to obtain them.

**What does `adCount` count?** Parsed ad placements in the returned HTML, not unique advertisers. Nested wrappers around the same marked ad are counted once. Separate top/bottom placements remain separate even when their URL and headline match. An empty `ads` array does not prove the full browser page had no ads; this parser makes no extra requests to load them.

**Is a page with a cookie-consent banner a failure?** No. The modern page sometimes carries a "Before you continue to Google" dialog on top of complete results; the Actor parses the results underneath. Only a consent page with no results is reported as blocked.

**What happens if Google shows a CAPTCHA or a consent page with no results?** The rejected page is stored in `DIAGNOSTIC-*` with `diagnosticReason: "SERP_FAILED"` and an explanatory `error`. When HTML is available, `debugHtmlKey` points to the saved `DEBUG_*.html` record. Inspect the diagnostics before retrying.

**Can it read Google's lightweight phone layout?** If Google returns that HTML, the parser reads its organic titles, real URLs, descriptions, displayed dates, sitelinks and related searches. Rating rows and navigation are excluded from descriptions. This is layout compatibility: it does not select a cheaper proxy route, change the requested device or guarantee a faster response. The existing `GOOGLE_SERP` transport and page pricing still apply.

**Does it return local businesses?** `localResults` contains business cards already visible in supported basic and lightweight SERP HTML, including `title`, `googleUrl`, numeric `rating` and `reviewsCount`, `priceRange`, `category`, `address` and `openingHoursText`. The Actor makes no extra place-detail requests. Businesses remain separate from `organicResults`; a page containing only identified business cards is a `features_only` SERP page and is billed once under the existing per-page price. Duplicate links to the same business card are removed.

Missing business fields are `null`, and an absent local pack is `[]`. Addresses and opening text are exactly the information shown by Google: an address can be only a town, and “Closed · Opens 11 AM” is a snapshot, not a verified weekly schedule. Ratings, counts and opening-text parsing have been checked on English captures; unsupported formats remain null. Phone numbers, websites, coordinates and detail-page enrichment are not included.

**What if Google changes its markup?** An unrecognized response is rejected rather than treated as a successful empty search. Inspect `DIAGNOSTIC-*`, its `diagnosticReason` and `error`, and any `debugHtmlKey`. With `debugHtml: true`, fetched HTML is also saved as `PAGE_<n>.html`. A recognized no-match page has `resultStatus: "no_results"`; a page containing useful widgets can be `features_only` even with no organic listings.

**Is this legal to run?** Scraping publicly visible search results is generally lower-risk than scraping logged-in content, but you are responsible for complying with Google's Terms of Service and your local laws for your use case.

**Can I export to CSV or Excel?** Yes, from the Output tab or the API, with a ready-made **Overview** view.

### Related Actors

- [Google Maps Scraper](https://apify.com/webdatatools/google-maps-scraper) — businesses with phone, website, address, rating and coordinates per search
- [Website to Markdown — Content Crawler for LLM & RAG](https://apify.com/webdatatools/website-to-markdown) — any site as clean Markdown per page, no browser
- [Web Scraper — CSS Selector & Data Extractor](https://apify.com/webdatatools/css-selector-extractor) — pull any CSS selector off any page, one row per URL
- [Structured Data & JSON-LD Extractor (Schema.org, Open Graph)](https://apify.com/webdatatools/structured-data-extractor) — Schema.org and Open Graph data from any page
- [AI Web Search & Read: Google results as clean Markdown](https://apify.com/webdatatools/ai-web-search) — a query turned into clean Markdown from the top search results

### Support and feedback

Found a query pattern that returns a parse error, a selector Google changed, or a field worth adding? Open an issue on the **Issues** tab — attach the `DEBUG_*.html` file from the run's key-value store if you have one.

If you have used this Actor, consider leaving an honest Store review describing what worked and what did not. No reward or positive rating is requested.

# Actor input Schema

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

Enter the Google search queries to scrape, e.g. apify web scraping. One dataset row is returned per accepted query/page. Verified zero-result pages remain billable; fetch, block, parsing and missing-destination failures are stored separately in diagnostics. Rows are saved as they finish; if the run timeout is near, the Actor stops early and still ends SUCCEEDED with the pages done so far.

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

Enter the 2-letter country code Google should localise results for, e.g. us, gb, de. Sent as the gl parameter.

## `languageCode` (type: `string`):

Enter the 2-letter interface language code, e.g. en, es, fr. Sent as the hl parameter and affects snippet language.

## `resultsPerPage` (type: `integer`):

Sent to Google as num, e.g. 10. Google currently ignores it and returns about 10 organic results per page (a 9 Oct 2026 test with 100 gave 9), so use Max pages per query to get more results.

## `maxPagesPerQuery` (type: `integer`):

Enter how many result pages to fetch per query, e.g. 1. Each extra page is the next ~10 results (start=10, 20, …) and a separate billed SERP page. Total pages = queries x this value. Google response and redirect waits vary; allow sufficient run timeout and inspect OUTPUT for partial results before retrying. Effective concurrency is capped at 3.

## `dateRange` (type: `string`):

Optional. Only results from the past hour, day, week, month or year (Google's Tools > Any time filter, sent as tbs=qdr:).

## `site` (type: `string`):

Optional. Restrict every query to one domain or TLD, e.g. reddit.com or gov — adds site: to each query.

## `fileType` (type: `string`):

Optional. Only documents of this type, e.g. pdf, docx, xlsx — adds filetype: to each query.

## `exactPhrase` (type: `string`):

Optional. A phrase every result must contain word for word — added to each query in quotes.

## `device` (type: `string`):

Choose desktop or mobile to control which User-Agent is sent — Google sometimes shows slightly different snippets/layout per device.

## `includePeopleAlsoAsk` (type: `boolean`):

Leave on to parse the People Also Ask box. The basic-HTML Google page this Actor fetches (for real result URLs) rarely contains it, so peopleAlsoAsk is usually empty; it is filled whenever Google does render the box. Does not change the price.

## `includeRelatedSearches` (type: `boolean`):

Leave on to extract related-search suggestions from Google's basic and modern widgets in the fetched HTML. Labels are deduplicated in page order; navigation and pagination are excluded. Google may omit suggestions. No extra source request or separate result charge.

## `safeSearch` (type: `boolean`):

Turn this on to request Google's SafeSearch filtering (adds safe=active to the request).

## `maxConcurrency` (type: `integer`):

SERP pages to fetch in parallel, e.g. 3. Effective concurrency is capped at 3; older inputs up to 10 remain accepted. After two rejected pages, queued pages are skipped while in-flight pages finish. Inspect OUTPUT for partial results before retrying.

## `debugHtml` (type: `boolean`):

Turn on only when reporting a parsing problem: saves every fetched Google page as PAGE\_<n>.html in the run's key-value store. Does not change the price or the results.

## Actor input object example

```json
{
  "queries": [
    "apify web scraping"
  ],
  "countryCode": "us",
  "languageCode": "en",
  "resultsPerPage": 10,
  "maxPagesPerQuery": 1,
  "device": "desktop",
  "includePeopleAlsoAsk": true,
  "includeRelatedSearches": true,
  "safeSearch": false,
  "maxConcurrency": 1,
  "debugHtml": false
}
```

# Actor output Schema

## `serpPages` (type: `string`):

All scraped SERP pages — download as JSON, CSV, Excel or HTML.

## `qualityReport` (type: `string`):

Confirmed pages, rejected pages, actual proxy attempts, cost-guard skipped pages and partial delivery state.

## `diagnostics` (type: `string`):

DIAGNOSTIC-\* keys contain rejected pages outside the result dataset.

# 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": [
        "apify web scraping"
    ],
    "countryCode": "us",
    "languageCode": "en",
    "resultsPerPage": 10,
    "maxPagesPerQuery": 1,
    "device": "desktop",
    "includePeopleAlsoAsk": true,
    "includeRelatedSearches": true,
    "safeSearch": false,
    "maxConcurrency": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("webdatatools/google-search-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": ["apify web scraping"],
    "countryCode": "us",
    "languageCode": "en",
    "resultsPerPage": 10,
    "maxPagesPerQuery": 1,
    "device": "desktop",
    "includePeopleAlsoAsk": True,
    "includeRelatedSearches": True,
    "safeSearch": False,
    "maxConcurrency": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("webdatatools/google-search-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": [
    "apify web scraping"
  ],
  "countryCode": "us",
  "languageCode": "en",
  "resultsPerPage": 10,
  "maxPagesPerQuery": 1,
  "device": "desktop",
  "includePeopleAlsoAsk": true,
  "includeRelatedSearches": true,
  "safeSearch": false,
  "maxConcurrency": 1
}' |
apify call webdatatools/google-search-scraper --silent --output-dataset

```

## MCP server setup

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