Google Search Results Scraper — SERP API
Pricing
from $4.50 / 1,000 results
Google Search Results Scraper — SERP API
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.
Pricing
from $4.50 / 1,000 results
Rating
0.0
(0)
Developer
Murat Uzun
Maintained by CommunityActor stats
0
Bookmarked
44
Total users
17
Monthly active users
a day ago
Last modified
Categories
Features
Share
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
- Enter your search terms into Search queries, e.g.
apify web scraping. One row comes back per query per result page. - Set Max pages per query (up to 10) if you need more than the first ~10 results — Google returns about 10 per page.
- Optionally narrow the search with Date range, Only this site, File type or Exact phrase.
- Pick Device (desktop or mobile User-Agent), click Start, then export as JSON, CSV, Excel or HTML — every
organicResults[].urlis 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
{"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):
{"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:
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:
import datetimeimport decimalimport jsonimport osfrom apify_client import ApifyClientclient = 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().itemsprint(items)
CLI, after authenticating your Apify CLI account:
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, Python ActorClient.call reference, CLI command 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.
{"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:
climatewithexactPhrase: "annual report",site: "gov",fileType: "pdf"anddateRange: "year": page 1 had 10 results, all on.govsites, all dated between 21 October 2025 and 29 September 2026, with PDF page counts in the snippets.- Paging past the first 10:
pizza deliverywithmaxPagesPerQuery: 2gave 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. resultsPerPageabove 10 does not help: withresultsPerPage: 100Google returned 9 results. Pages are therefore requested atstart=10, 20, …whateverresultsPerPagesays, so no results are skipped between pages; usemaxPagesPerQueryfor 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 (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 — businesses with phone, website, address, rating and coordinates per search
- Website to Markdown — Content Crawler for LLM & RAG — any site as clean Markdown per page, no browser
- Web Scraper — CSS Selector & Data Extractor — pull any CSS selector off any page, one row per URL
- Structured Data & JSON-LD Extractor (Schema.org, Open Graph) — Schema.org and Open Graph data from any page
- AI Web Search & Read: Google results as clean Markdown — 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.
