# Web Search - Google Results by Country and Language (`leadproof/web-search`) Actor

Batch Google search with an explicit country and language. One result record per query: ranked organic results (URL, title, snippet), ads excluded, redirect links resolved, and failures, retries and provider costs reported honestly.

- **URL**: https://apify.com/leadproof/web-search.md
- **Developed by:** [Lead Proof](https://apify.com/leadproof) (community)
- **Categories:** SEO tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 search results pages

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

## Web Search - Google Results by Country and Language

Search Google for a list of queries from the country and in the language you choose. You get one record per
query: the organic results ranked 1, 2, 3... (URL, title, snippet), with ads left out and Google's redirect links
resolved to the real page URLs. Failed, blocked and empty searches are reported as such, never as silent zero
results.

Built for lead research and enrichment pipelines that need to know **which country's Google they searched**:
the country is always an explicit setting, never a guess from wherever a proxy happens to exit.

### What it does

- Sends each query to the country's Google domain (for Kenya `www.google.co.ke`) with `gl=<country>`,
  `hl=<language>` and the Apify Google SERP proxy's `country` parameter, and reports all of them.
- Parses organic results only. Text ads, People also ask, carousels, maps and side panels are excluded, even
  when Google places an ad inside the results list.
- Resolves Google's `/goto` redirect links with one request to Google per link (the result page itself is never
  opened), strips `#:~:text=` highlight fragments and removes duplicate URLs within a query.
- Follows result pages (`start=10, 20...`) until it has your number of results, Google has no next page, or the
  page cap is reached.
- Retries only transient failures (timeouts, rate limits, CAPTCHA pages, proxy errors) a bounded number of times,
  and lists every attempt.
- Keeps input order and your ids. Repeated queries are searched again, each under its own id.
- Resumes after a crash, abort or migration without searching finished queries again.

It does not crawl result pages, solve CAPTCHAs or log in anywhere.

### Input

```json
{
  "queries": [
    {"id": "kenyan-business", "query": "\"Eon Energy\" Kenya"},
    {"id": "location-kisumu", "query": "hardware shops in Kisumu"},
    "maduka ya vifaa vya ujenzi Kisumu"
  ],
  "country": "KE",
  "language": "en",
  "maxResultsPerQuery": 10
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `queries` | required | Up to 500 strings or `{id, query}` objects. Ids must be unique when given; rows without one get `row-<position>`. |
| `country` | required | ISO 3166-1 alpha-2 (`KE`, `NG`, `GB`, `US`...). English names also work. |
| `language` | required | Google interface language (`en`, `sw`, `fr`, `pt-BR`...). |
| `maxResultsPerQuery` | 10 | 1-100. More than 10 needs more result pages, each a separate search request. |
| `maxPagesPerQuery` | 0 (automatic) | Hard cap on result pages, and so on search requests, per query (max 10). |
| `dateRange` | `any` | `day`, `week`, `month`, `year` (Google's time filter). |
| `site` | none | Restrict to one domain, optionally with a path; added as `site:`. |
| `restrictToLanguage` | false | Only documents in `language` (Google `lr`). |
| `resolveRedirects` | true | Off returns Google's redirect links unchanged (with a warning). |
| `provider` | `google` | `brave` uses the Brave Search API with your key (see below). |
| `maxConcurrency` | 3 | Parallel queries, 1-10. |
| `maxRetries` | 2 | Per result page, transient failures only. |
| `requestTimeoutSecs` | 45 | Per search request. The proxy commonly answers in 4-20 s; one pilot request needed a retry after 45 s. |

#### How to use it

1. Put your queries in `queries`, as strings or `{"id", "query"}` objects so the results join back to your rows.
2. Set `country` and `language`. Both are required, so every run states which Google it searched.
3. Leave `maxResultsPerQuery` at 10 for one results page per query, or raise it to page further (each page is one
   more search request).
4. Start the run, then read the dataset (one record per query), the CSV link in the Output tab (one row per
   result) and the `SUMMARY` record for counts, stop reason and request costs.

### Output

One dataset item per query (a SearchEnvelope, contract v1.0). Shortened from pilot run `rOTeqvieMHskxsp2a`:

```json
{
  "schemaVersion": "1.0",
  "inputId": "kenyan-business",
  "status": "succeeded",
  "warnings": [],
  "error": null,
  "queryId": "kenyan-business",
  "query": "\"Eon Energy\" Kenya",
  "country": "KE",
  "language": "en",
  "provider": "google-serp-apify-proxy",
  "results": [
    {"rank": 1, "url": "https://eon.co.ke/", "title": "Eon Energy Ltd - Website",
     "snippet": "P. O. Box 75014 - 00200, Nairobi, Kenya. Call Us. Tel: +254 20 2210 028; Email: info [at] eon.co.ke. ...",
     "page": 1, "urlSource": "resolved_redirect", "displayUrl": "https://eon.co.ke", "language": "en"},
    {"rank": 4, "url": "https://ke.linkedin.com/company/eon-energy-limited", "title": "EON ENERGY LIMITED", "...": "..."}
  ],
  "resultCount": 10,
  "engine": "google",
  "appliedSettings": {"googleHost": "www.google.co.ke", "hl": "en", "gl": "ke", "proxyGroup": "GOOGLE_SERP",
                      "proxyCountry": "KE", "resultsPerPage": 10},
  "observedSettings": {"pageLanguage": "en-KE", "hl": "en", "gl": "ke"},
  "attempts": [{"page": 1, "attempt": 1, "outcome": "ok", "httpStatus": 200, "elapsedMs": 15270, "billable": true}],
  "usage": {"providerRequests": 1, "billableRequests": 1, "pagesRetrieved": 1, "redirectLookups": 10,
            "estimatedProviderCostUsd": 0.0025},
  "searchedAt": "2026-09-19T19:55:17.717Z",
  "inputIndex": 0
}
```

- **status** is about retrieval, not relevance: `succeeded` (every planned page retrieved; zero results is a valid
  empty search), `partial` (some results, but a later page failed or some links stayed unresolved), `failed` (no
  usable results page; `error` says why and whether a retry may help). `skipped` exists in the contract for
  library callers; this Actor leaves unstarted queries out of the dataset and lists them in `SUMMARY`.
- **rank** is the position within the results returned for this query after exclusions and deduplication. It
  is not a stable Google rank. The same query twice can return different results: in the pilot the repeated
  query matched in all 10 positions, in a local run the same day in 9 of 10. That is an observation, not a
  guarantee of stable results.
- **warnings** are `{code, message}`: `ads_excluded`, `local_results_excluded` (Google Maps local listings shown on
  the page, not returned), `knowledge_panel_excluded` (links from a knowledge panel, not returned), `exact_phrase_not_found` (no page matched a quoted name, so Google showed results for the
  query without quotes; `correctedQuery` has that query), `query_auto_corrected`, `query_changed_by_engine`,
  `unresolved_redirect_urls`, `observed_language_differs`, `observed_country_differs`, `page_failed`,
  `country_google_domain_fallback`...
- **error.code** values: `captcha`, `rate_limited`, `timeout`, `network_error`, `provider_error`,
  `provider_auth_failed`, `empty_response`, `markup_changed`, `unrecognized_page`, `consent_required`,
  `js_required`, `invalid_query`.
- `observedSettings` is what the results page reported about itself (interface language and country in Google's
  own links). It is a diagnostic: the proxy's exit location cannot be verified from the response.

Views: **Queries** (one row per query) and **Results** (one row per result). The CSV link in the run's Output tab
gives one row per result. The key-value record `SUMMARY` has counts by status, failure codes, stop reason,
request counts and the estimated provider cost.

### Costs (measured, not a price list)

Measured on the LeadProof account (Apify Starter plan) on 2026-09-19. The search itself is Apify's Google SERP
proxy, billed per request; it is not free and not ours.

| Run | Queries | Search requests (`PROXY_SERPS`) | Run time | Total platform usage |
| --- | --- | --- | --- | --- |
| `rOTeqvieMHskxsp2a` pilot | 3 (1 page each) | 3 | 21 s | $0.0086 |
| `QGErJ3xDTWpTfSRNl` abort + resurrect | 6 | 6 (none repeated) | 69 s | $0.0177 |
| `MN1Y8lK1bFxt0it4E` Swahili, 2 pages | 1 | 2 | 27 s | $0.0061 |
| `0I5V0LMvVHd9Ba9UY` `site:` filter | 1 | 1 | 13 s | $0.0031 |
| `gbL8xzbgKbKAZ48Vh` Console prefill | 2 | 2 (plus 1 timed-out attempt, not billed) | 75 s | $0.0075 |

Per result page: $0.0025 for the SERP request (this account's plan price, equal to Apify's pay-per-event developer
cost for FREE/BRONZE users; $0.002 SILVER, $0.0017 GOLD) plus about $0.0002-0.0003 of compute and $0.00005 per
key-value write. Pagination multiplies the SERP cost by the number
of pages. Retries of failed requests are extra requests; in these runs a timed-out request was not billed, but
count on each retry as a possible extra SERP. Redirect resolution adds no SERP cost (a few seconds of run time).
Each run also has a fixed start cost of about $0.0003-0.0005.

### What you are charged for

One event, `search-page`, at **$0.005**: one results page that was retrieved and parsed, with up to 10 organic
results. Apify's standard `apify-actor-start` event ($0.00005) covers the run's first seconds of compute. There is
no per-dataset-item charge.

- A search that legitimately finds nothing (Google's own empty results page) **is charged**: the request was made
  and answered, and the record tells you the page was empty.
- A technical failure is **not charged**: CAPTCHA, consent or JavaScript pages, timeouts, proxy and network
  errors, an empty body, or markup we cannot parse. Those records carry `status: failed` and an `error`.
- **Retries are never an extra event.** A page that needed two or three attempts is still one event; the extra
  requests are ours.
- More results means more pages: `maxResultsPerQuery` 10 is one event per query, 30 is up to three, and the
  event count always equals `usage.pagesRetrieved` in the record. `maxPagesPerQuery` caps it.
- A query stopped by your run's maximum cost is not charged and is listed in `SUMMARY.notProcessedInputIds`.

### Fair use

Queries go to Google through Apify's Google SERP proxy, which bills per request, and each result link is resolved
with one request to Google. Nothing else is fetched: result pages are never opened, there is no login and no
attempt to defeat a block. CAPTCHA and consent pages are reported as failures, never solved. A run is as public
as an ordinary Google search; use the results within Google's terms and your own legal advice.

### Related LeadProof Actors

- [Business Enrichment](https://apify.com/leadproof/business-enrichment): company profile and contacts from a name or website.
- [Website Email Finder](https://apify.com/leadproof/website-email-finder) and [Bulk Email Verifier](https://apify.com/leadproof/bulk-email-verifier): find and check contact addresses.
- [Google Maps Scraper](https://apify.com/leadproof/google-maps-scraper) for the local business listings this Actor deliberately keeps out of organic results.
- Ready-made lead lists: [leadproof.co](https://leadproof.co).

### Other provider: Brave Search API

`provider: "brave"` uses Brave's own index (results are labeled `engine: "brave"`, never Google). It needs the
`BRAVE_SEARCH_API_KEY` secret environment variable (a Brave `X-Subscription-Token`; Brave lists $5 per 1,000
requests with $5 of monthly credit and asks for a card). Brave accepts 38 country codes (Kenya is not one of them:
the Actor sends `ALL` and warns) and about 50 search languages. It is tested with documented response shapes only;
no key was available to test it live.

### Limitations

- Google changes its result markup. A page with result links that cannot be parsed fails with `markup_changed`
  instead of returning zero results; parsing needs maintenance when that happens.
- Results are Google's personalised-by-location ranking at that moment, not a complete index of the web. Local
  map packs, place cards and knowledge panels are not returned (organic results only); a page that only has them
  is a valid empty search with a `local_results_excluded` or `knowledge_panel_excluded` warning. Those warnings
  say a business card appeared on the page and was not extracted; whether it matches your query is not verified.
- For a quoted name with no exact match, Google silently searches without the quotes. Such results are returned
  with `exact_phrase_not_found`, so they must not be read as exact-name matches.
- A result can be in another language than you asked for (Google's own `lang` tag is in `results[].language`).
- Result links are resolved through Google. If Google stops answering those requests, links stay as Google
  redirect URLs, the envelope is `partial` and says so.
- CAPTCHA pages are retried, not solved.
- Up to 500 queries and 100 results (10 pages) per query per run.

### Development

```bash
python -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m unittest discover -s test          # fixtures only, no network
python ../../.github/scripts/check_actor.py .           # the CI check
```

The search engine is a library (`websearch/`) that other Actors can import without starting this Actor; see
`SESSION_HANDOFF.md`. The contract schemas and example envelopes are in `websearch/schemas/` and `contract/`.
Third-party packages and licenses: `THIRD_PARTY_LICENSES.md`.

# Actor input Schema

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

Up to 500 queries, as strings or objects {"id": "your-id", "query": "..."}. Order is kept, and repeated queries are searched again with their own ids. Ids must be unique when given.

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

Country to search from, as an ISO 3166-1 alpha-2 code such as KE, NG, GB or US (English country names also work). Sent to Google as the country domain and gl parameter, and to the proxy as its country. Required: never guessed.

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

Interface language code such as en, sw, fr or pt-BR (Google hl). Results in other languages can still appear unless you restrict them below.

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

Organic results to keep per query. Google shows up to 10 per page, so more than 10 needs more result pages, and each page is a separate search request.

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

Hard cap on result pages (and paid search requests) per query. 0 means just enough pages for the maximum results. Pagination also stops when Google has no next page.

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

Google's time filter (tbs=qdr).

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

Restrict every query to one domain, optionally with a path, for example kenyabizlist.com. Added to the query as a site: operator.

## `restrictToLanguage` (type: `boolean`):

Ask Google for documents in the selected language only (lr parameter).

## `resolveRedirects` (type: `boolean`):

Google links results through its own redirect. On: each link is resolved with one request to Google that reads only the destination (target pages are never opened). Off: redirect links are returned as they are.

## `provider` (type: `string`):

Google uses the run's Apify Google SERP proxy. Brave is a different engine with its own index; it needs the BRAVE\_SEARCH\_API\_KEY secret and results are labeled brave.

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

Queries searched at the same time.

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

Retries for transient failures only (timeouts, rate limits, CAPTCHA pages, proxy errors). Each retry is another search request.

## `requestTimeoutSecs` (type: `integer`):

Timeout for one search request. Google SERP proxy requests commonly take 4-20 seconds.

## Actor input object example

```json
{
  "queries": [
    {
      "id": "eon-energy",
      "query": "\"Eon Energy\" Asumbi Kenya"
    },
    {
      "id": "hardware-kisumu",
      "query": "hardware shops in Kisumu"
    }
  ],
  "country": "KE",
  "language": "en",
  "maxResultsPerQuery": 10,
  "maxPagesPerQuery": 0,
  "dateRange": "any",
  "restrictToLanguage": false,
  "resolveRedirects": true,
  "provider": "google",
  "maxConcurrency": 3,
  "maxRetries": 2,
  "requestTimeoutSecs": 45
}
```

# Actor output Schema

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

Full records: status, results, applied and observed settings, attempts and usage.

## `csv` (type: `string`):

One row per organic result.

## `summary` (type: `string`):

Counts by status, stop reason, requests, redirect lookups and estimated provider cost.

# 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": [
        {
            "id": "eon-energy",
            "query": "\"Eon Energy\" Asumbi Kenya"
        },
        {
            "id": "hardware-kisumu",
            "query": "hardware shops in Kisumu"
        }
    ],
    "country": "KE",
    "language": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadproof/web-search").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": [
        {
            "id": "eon-energy",
            "query": "\"Eon Energy\" Asumbi Kenya",
        },
        {
            "id": "hardware-kisumu",
            "query": "hardware shops in Kisumu",
        },
    ],
    "country": "KE",
    "language": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("leadproof/web-search").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": [
    {
      "id": "eon-energy",
      "query": "\\"Eon Energy\\" Asumbi Kenya"
    },
    {
      "id": "hardware-kisumu",
      "query": "hardware shops in Kisumu"
    }
  ],
  "country": "KE",
  "language": "en"
}' |
apify call leadproof/web-search --silent --output-dataset

```

## MCP server setup

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

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/awPEaOi7dlBxhTTrC/builds/U5tMaW2mbhGX0BxAV/openapi.json
