> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scrapeunblocker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Google SERP scraping

> Scrape Google search results - organic, ads, totals - through the /serpApi endpoint.

The `/serpApi` endpoint scrapes Google search results and returns normalized JSON. It handles the CAPTCHA gauntlet, ad rendering delays, and proxy rotation that make Google scraping painful.

## Minimum request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.scrapeunblocker.com/serpApi?keyword=best+running+shoes" \
    -H "x-scrapeunblocker-key: YOUR_API_KEY"
  ```

  ```python Python theme={null}
  import requests

  r = requests.post(
      "https://api.scrapeunblocker.com/serpApi",
      params={"keyword": "best running shoes"},
      headers={"x-scrapeunblocker-key": "YOUR_API_KEY"},
      timeout=120,
  )
  data = r.json()
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    "https://api.scrapeunblocker.com/serpApi?keyword=best%20running%20shoes",
    {
      method: "POST",
      headers: { "x-scrapeunblocker-key": "YOUR_API_KEY" },
    }
  );
  const data = await res.json();
  ```
</CodeGroup>

## Response shape

```json theme={null}
{
  "keyword": "best running shoes",
  "proxyCountry": "us",
  "totalResults": 458000000,
  "topAdsCount": 1,
  "bottomAdsCount": 0,
  "organicResultsCount": 1,
  "resultsCollected": 1,
  "topAds": [
    {
      "title": "Nike Run Club - Free Shipping",
      "url": "https://www.nike.com/...",
      "displayedUrl": "nike.com",
      "description": "Shop the latest running shoes...",
      "position": 1
    }
  ],
  "bottomAds": [ ... ],
  "organic": [
    {
      "title": "10 Best Running Shoes of 2026",
      "url": "https://runnersworld.com/...",
      "displayedUrl": "runnersworld.com",
      "description": "Our experts tested...",
      "position": 1
    }
  ],
  "aiOverview": {
    "text": "Running shoes are generally chosen by gait, cushioning and...",
    "sources": ["runnersworld.com", "reddit.com"]
  }
}
```

Every response carries `keyword`, `proxyCountry`, `totalResults`, the count
fields (`topAdsCount`, `bottomAdsCount`, `organicResultsCount`,
`resultsCollected`), the three result lists (`topAds`, `bottomAds`, `organic`)
and `aiOverview`. On a country fallback it also carries `requestedCountry` and
`note` (see [`proxy_country`](#proxy_country) below).

### `aiOverview`

Google's AI-written summary above the results, plus the hostnames it cites, or
`null` when the search does not have one. Whether Google generates an overview
depends on the query and the region, so treat this as an extra rather than a
guarantee - a commercial query usually has none, an informational one usually
does.

It is read from the page we already fetched and never waited for, so it costs
you no extra time. In rare cases where the overview is still being written when
the page is captured, the field is `null`.

<Note>
  `topAds` and `bottomAds` cover the paid units wherever Google puts them,
  including the blocks it now renders inside the organic column. Anything paid is
  in one of those two lists and never in `organic`.
</Note>

## Useful parameters

### `pages_to_check`

How many SERP pages to scrape, 1 to 10. Defaults to 1.

```bash theme={null}
curl -X POST "https://api.scrapeunblocker.com/serpApi?keyword=running+shoes&pages_to_check=3" \
  -H "x-scrapeunblocker-key: YOUR_API_KEY"
```

Results from all pages are concatenated into `topAds`, `bottomAds`, and `organic` with `position` reflecting cross-page ordering.

### `proxy_country`

Two-letter ISO code forcing the request through that country's IP pool. Affects what Google shows you - SERP composition varies by region.

```bash theme={null}
curl -X POST "https://api.scrapeunblocker.com/serpApi?keyword=pizza&proxy_country=de" \
  -H "x-scrapeunblocker-key: YOUR_API_KEY"
```

**Available countries (ISO2):** `BE`, `CA`, `CH`, `CN`, `DE`, `DK`, `ES`, `FR`,
`GB`, `HK`, `HR`, `IE`, `IT`, `JP`, `KR`, `LT`, `NL`, `NO`, `PL`, `RO`, `SE`,
`SG`, `TW`, `US`.

**Country fallback.** If you request a country we do not have an exit for, the
request does **not** error. The results are served from an available exit, and
the JSON response carries two extra fields so you know it happened:

| Field              | Type   | Meaning                                     |
| ------------------ | ------ | ------------------------------------------- |
| `requestedCountry` | string | The ISO2 country you asked for.             |
| `note`             | string | Human-readable explanation of the fallback. |

```json theme={null}
{
  "keyword": "pizza",
  "proxyCountry": "de",
  "requestedCountry": "il",
  "note": "Country 'IL' is not available. Results were served from 'DE'. Available countries: BE, CA, CH, CN, DE, DK, ES, FR, GB, HK, HR, IE, IT, JP, KR, LT, NL, NO, PL, RO, SE, SG, TW, US.",
  "organic": [ ... ]
}
```

<Note>
  `requestedCountry` and `note` appear **only** on a country fallback. On a
  normal request where your `proxy_country` is available, neither field is
  present and `proxyCountry` echoes the country you asked for.
</Note>

### `wait_after_load`

Extra seconds to wait before extraction, on top of the wait we already do.

You should not normally need it: extraction waits for the paid units to finish
arriving before reading the page, so ads are not missed because of timing. Reach
for this only when you have a specific reason to let a SERP settle longer - it
is added to your response time as-is.

```bash theme={null}
curl -X POST "https://api.scrapeunblocker.com/serpApi?keyword=insurance&wait_after_load=3" \
  -H "x-scrapeunblocker-key: YOUR_API_KEY"
```

### `captcha_pause`

Seconds to pause if Google shows a CAPTCHA, giving an interactive user time to solve it. Only useful when running the API in attended mode. Leave at 0 for automated workloads - rely on `proxy_country` rotation instead.

## Errors

| Code  | Meaning                                                                                                                        |
| ----- | ------------------------------------------------------------------------------------------------------------------------------ |
| `403` | Google CAPTCHA after retries on multiple proxies. Try a different `proxy_country` or wait.                                     |
| `502` | Zero results with no "no results" notice on the page, on two separate fetches - an upstream block, not an empty search. Retry. |
| `504` | SERP fetch timed out. Lower `pages_to_check` or retry.                                                                         |
| `422` | Missing `keyword` or invalid parameter range                                                                                   |

A keyword Google genuinely has no results for is **not** an error: you get `200`
with `resultsCollected: 0` and an empty `organic` array. This is normal for narrow
`site:` queries, and it means you can trust an empty result set instead of retrying it.

## Tips

* **Phrase keywords like a user would.** `keyword=best+pizza+nyc` performs differently from `keyword=best%20pizza%20nyc%202026`. Use spaces or `+`, not commas.
* **Watch `totalResults` for sanity.** A `totalResults` of 0 with valid organic results means the count couldn't be extracted - not that there are no results.
* **Cross-page dedup is your job.** If you call with `pages_to_check=10`, expect some overlap on borderline results. Dedup on `url`.
