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

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Search engines: results and rankings

> Get Google and Bing results as structured data with Zenrows, for any search and any country.

Search results pages (SERPs) feed rank tracking, SEO research, brand monitoring and research agents. Send the search URL to Fetch and get the results back as JSON, localized to the market you track.

**Uses:** [Fetch](/fetch/api-reference) with [Adaptive Stealth Mode](/fetch/features/adaptive-stealth-mode) (`mode=auto`), [Extract](/extract/introduction) (`extract=auto`), [`proxy_country`](/fetch/features/proxy-country), [`css_extractor`](/fetch/features/css-extractor)

## Ask your AI agent

With the [Zenrows MCP server](/mcp/overview) connected to Claude, Cursor or any MCP client, paste this prompt. Your agent picks the right Zenrows tools and settings on its own.

```text theme={"dark"}
Use Zenrows to read the Google results for https://www.google.com/search?q=pour+over+coffee+ratio&hl=en&gl=us&num=10
as a searcher in the US. List the organic results with their position, title and source,
then the "People also ask" questions.
```

## Get Google results as JSON

Build the Google URL with your query and its localization parameters, then send it with `extract=auto`. `parsed` comes back with one group per kind of result on the page, such as `organic_results`, `related_questions` and `video_results`.

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  from urllib.parse import urlencode
  import requests

  query = {"q": "pour over coffee ratio", "hl": "en", "gl": "us", "num": 10}
  google_url = "https://www.google.com/search?" + urlencode(query)

  params = {
      "apikey": "YOUR_ZENROWS_API_KEY",
      "url": google_url,
      "mode": "auto",
      "proxy_country": "us",
      "extract": "auto",
  }

  response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
  response.raise_for_status()
  parsed = response.json()["parsed"]

  print("Groups on this page:", sorted(k for k, v in parsed.items() if v))

  # Organic results come in a list group whose items have a title and a URL
  for group, items in parsed.items():
      if group.endswith("_results") and isinstance(items, list):
          for item in items:
              if item.get("title") and item.get("url"):
                  print(f"[{group}] #{item.get('position')} {item['title']} | {item.get('source_name')}")
  ```

  ```bash cURL theme={"dark"}
  curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.google.com%2Fsearch%3Fq%3Dpour%2Bover%2Bcoffee%2Bratio%26hl%3Den%26gl%3Dus%26num%3D10&mode=auto&proxy_country=us&extract=auto"
  ```
</CodeGroup>

The groups present depend on what Google shows for that query and market, so the loop reads every `*_results` group instead of a fixed name. Results-page layouts vary from query to query, which is why this example skips `extract_fields`.

## Example output

```text theme={"dark"}
Groups on this page: ['organic_results', 'related_questions']
[organic_results] #6 What is your typical ratio? : r/pourover | Reddit · r/pourover
[organic_results] #10 Guide To Pour-Over Coffee | Counter Culture Coffee
[organic_results] #12 How to Make a Perfect Pour Over Coffee | Brewing Tips | Stone Creek Coffee
[organic_results] #16 Coffee to Water Ratio Calculator | Coffee Bros.
```

`position` follows the order of every block on the page, including the questions and videos between organic results, so sort by it to get the ranking. The second result above in full:

```json theme={"dark"}
{
  "date": null,
  "displayed_url": "https://counterculturecoffee.com › counter-culture-coffee",
  "position": 10,
  "snippet": "We recommend somewhere between 1:14 and 1:20. That means that for every 1 gram of coffee you will want to use between 14...",
  "source_name": "Counter Culture Coffee",
  "title": "Guide To Pour-Over Coffee",
  "url": "/goto?url=CAES1AEB6zswFY5Kqdli7UzLSTX8q1b..."
}
```

`url` is Google's own redirect link, as it appears on the page. Prefix it with `https://www.google.com` to follow it, and read `displayed_url` or `source_name` for the destination site.

## Localize results to a market

Two layers decide which results you get: the parameters in the Google URL, and where the request comes from. Set both to the same market.

| Setting | Where | Example | Effect |
| - | - | - | - |
| `hl` | Google URL | `hl=de` | Interface language |
| `gl` | Google URL | `gl=de` | Country the results are tuned for |
| `num` | Google URL | `num=10` | Results per page |
| `start` | Google URL | `start=10` | Offset for the next page of results |
| `proxy_country` | Zenrows parameter | `proxy_country=de` | Sends the request from that country |

For German results, set `hl=de` and `gl=de` in the Google URL and send it with `proxy_country=de`.

## Get Bing results with CSS selectors

For Bing, pick the fields with `css_extractor`. Each organic result is an `li.b_algo` element, and its link is a Bing redirect that carries the destination URL in base64:

```python Python (Requests) theme={"dark"}
import base64, json
from urllib.parse import parse_qs, urlencode, urlparse
import requests

def real_url(bing_link):
    # Bing wraps each result in a /ck/a redirect; the target is base64 in the "u" parameter
    u = parse_qs(urlparse(bing_link).query).get("u", [""])[0]
    if not u.startswith("a1"):
        return bing_link
    encoded = u[2:]
    return base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)).decode()

selectors = {"titles": "li.b_algo h2", "links": "li.b_algo h2 a @href"}
bing_url = "https://www.bing.com/search?" + urlencode({"q": "how to brew pour over coffee", "setlang": "en", "cc": "us"})

params = {
    "apikey": "YOUR_ZENROWS_API_KEY",
    "url": bing_url,
    "mode": "auto",
    "proxy_country": "us",
    "css_extractor": json.dumps(selectors),
}

response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
response.raise_for_status()
data = response.json()

for position, (title, link) in enumerate(zip(data["titles"], data["links"]), start=1):
    print(position, title, "|", real_url(link))
```

```text theme={"dark"}
1 How to Make the Best Pour-Over Coffee at Home - Serious Eats | https://www.seriouseats.com/make-better-pourover-coffee-how-pourover-works-temperature-timing
2 How to Make Pour Over Coffee: Step-by-Step Brewing Guide | https://coffeehomebrewing.com/how-to-make-pour-over-coffee/
3 How to Make Pour Over Coffee: A Complete Step-by-Step Guide | https://brewingcoffees.com/how-to-make-pour-over-coffee/
4 The Best Way to Make Pour Over Coffee - Real Simple | https://www.realsimple.com/food-recipes/cooking-tips-techniques/pour-over-coffee
```

`setlang` and `cc` set Bing's language and country. Pair them with a matching `proxy_country`.

## Track many keywords

Rank tracking means the same query list every day. Put one search URL per keyword in a [Batch](/academy/scrape-from-a-list-of-urls) job with `zenrows_params` set to `{"mode": "auto", "proxy_country": "us", "extract": "auto"}`, and give each task the keyword as its `external_id`. To run it daily on Zenrows' side, see [Run scraping jobs on a schedule](/academy/batch/scheduled-jobs).

## Customize it

| Change | How |
| - | - |
| Another country or language | Change `hl`, `gl` and `proxy_country` together, see [Proxy country](/fetch/features/proxy-country) |
| Page 2 and beyond | Add `start=10`, `start=20` to the Google URL |
| The full page for an LLM | `response_type=markdown` instead of `extract=auto`, see [Markdown for LLMs](/academy/fetch/markdown-for-llms) |
| Only titles and links from Google | `css_extractor`, see [CSS extractor](/fetch/features/css-extractor) |
| Read the pages behind the results | Send the result URLs to [Batch](/academy/scrape-from-a-list-of-urls) with `response_type=markdown` |

## Troubleshooting

* **Results are for the wrong country**: the URL and the request location disagree. Set `gl` and `proxy_country` to the same country.
* **A group your code expects is missing**: Google shows different blocks for different queries. Loop over the `*_results` groups, as in the first example, instead of hard-coding one name.
* **Bing returns results for a shorter query**: Bing rewrites some queries. Make the query more specific, or add `setlang` and `cc` to pin the market.
* **The request takes long to return**: set your client timeout to 180 seconds or more, so `mode=auto` has time to pick the configuration that works.

## Related recipes

<CardGroup cols={2}>
  <Card title="Build a research agent with live web data" icon="robot" href="/academy/pipelines/research-agent">
    Search, read the top results and cite sources.
  </Card>

  <Card title="Get structured JSON from any page" icon="wand" href="/academy/extract/auto-extraction">
    How extract=auto and prepared domains work.
  </Card>

  <Card title="Get content as seen from any country" icon="world" href="/academy/fetch/geotargeting">
    Geotarget any request with proxy\_country.
  </Card>

  <Card title="Detect changes on any page and get alerted" icon="bell" href="/academy/pipelines/change-detection">
    Alert when rankings or content change.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.