> Searlo API Docs — Full SERP
> Source: https://searlo.tech/docs#full-serp
> Full docs: https://searlo.tech/docs/full.md

# Full SERP

Google's whole results page as JSON: organic results, videos, ads, People Also Ask, related searches, the knowledge graph and the AI Overview. Each page is fetched live from Google and parsed, in a few seconds, AI Overview included: the overview is read in full from the page as Google served it. An overview Google loads after the page is read from the rendered page instead, which adds several seconds. [Web Search](#search-web) returns organic results only, for 1 credit, in about a second.

### Endpoints

```
GET  /serp
POST /serp/batch
GET  /serp/features
GET  /search/news/serp
```

> `GET /search/serp` is an alias of `GET /serp`: same parameters, same price.

**Credits:** 3 per page delivered (`pages` asks for up to 10; a page Google does not have, or one that repeats the page before it, is not billed), +1 with `rawHtml=true`. A SERP with no results is not charged. An AI Overview read in full from the page as served is included at no extra charge. **From 2026-11-01**, a request whose AI Overview had to be completed by rendering the page costs 5 more (8 for a one-page request), and the response says `aioverview.rendered: true`; with `aioverview=false` it never does. Until then it stays 3. A cached result is billed like a live one, rendered or not. `credits` in the body and the `X-Credits-Deducted` header say what was charged.

Search operators (`site:`, `intitle:`, `inurl:`, `filetype:`, `before:`/`after:`), e-mail lookups and long OR chains are not served: they answer `422` `QUERY_NOT_SERVED` with `retryable: false`, not charged.

> Want Google's page itself rather than JSON, or the page an iPhone gets? See [SERP HTML](#serp-html).

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `q` | string | Yes | Search query (1–400 chars) |
| `gl` | string | No | Country, 2 letters (default: `us`) |
| `hl` | string | No | Language, 2–5 chars (default: `en`) |
| `location` | string | No | A country, by name or two-letter code (max 120 chars), e.g. `Germany` or `DE`. Applied as `gl`, and takes precedence over it. A city or state returns `400` `CITY_LOCATION_UNAVAILABLE` (see [Location](#full-serp)); an unknown name returns `400` `UNKNOWN_LOCATION` with `suggestions`. Neither is charged. Cannot be combined with `uule` |
| `uule` | string | No | A Google `uule` token (max 300 chars). A country's token is applied as that country's `gl`; any other token (a city, a state, coordinates) returns `400` `CITY_LOCATION_UNAVAILABLE` |
| `device` | string | No | `desktop` (default). `mobile` and `tablet` are accepted but return the desktop layout; for the page an iPhone gets, use [SERP HTML](#serp-html) with `device=ios` |
| `page` | number | No | First results page to fetch (1–100, default: 1) |
| `pages` | number | No | Consecutive pages to fetch (1–10, default: 1). Each page delivered is billed; `pagesFetched` says how many came back |
| `num` | number | No | Results per page (1–10, default: 10). Google serves at most 10 per page: ask for more with `pages` |
| `tbs` | string | No | Google time or sort filter, max 40 chars. Only `qdr:h`, `qdr:d`, `qdr:w`, `qdr:m` and `qdr:y` (optionally with a number, e.g. `qdr:d3`), `li:1` and `sbd:1` are applied; other values are ignored |
| `safe` | string or boolean | No | `active` (or `true`) turns SafeSearch on; `off` (or `false`, the default) leaves it off. Cached apart from SafeSearch-off results |
| `aioverview` | boolean | No | Include the AI Overview (default: **true**). `false` drops the `aioverview` key and skips reading it in full, which makes the call faster |
| `googleGoto` | boolean | No | Resolve Google `/goto` redirect links in the AI Overview sources (default: true) |
| `googleGotoOrganic` | boolean | No | Also resolve `/goto` links on organic results (default: false) |
| `cache` | boolean | No | `false` skips the cache and fetches live (default: true) |
| `rawHtml` | boolean | No | `true` adds `rawHtml`: the page's HTML as Google served it, alongside the JSON (+1 credit). A result with the page is cached separately from one without it. For the rendered page, use [SERP HTML](#serp-html) with `js=true` |

### Response

```json
{
  "success": true,
  "searchParameters": { "q": "best running shoes", "gl": "us", "hl": "en", "page": 1, "pages": 1, "num": 10, "device": "desktop", "type": "serp" },
  "organic": [
    {
      "position": 1,
      "title": "The Best Running Shoes of 2026",
      "url": "https://www.example.com/best-running-shoes",
      "redirectUrl": null,
      "resolved": true,
      "displayedUrl": "https://www.example.com › reviews",
      "domain": "example.com",
      "snippet": "We ran more than 500 miles in 40 pairs...",
      "date": "Sep 12, 2026",
      "sitelinks": [{ "title": "Road shoes", "url": "https://www.example.com/road" }],
      "page": 1
    }
  ],
  "videos": [
    { "position": 1, "title": "Best Running Shoes 2026", "url": "https://www.youtube.com/watch?v=abc123", "redirectUrl": null, "resolved": true, "source": "YouTube", "channel": "Example Running" }
  ],
  "ads": [
    { "position": 1, "block": "top", "title": "Running Shoes Sale", "url": "https://shop.example.com/", "displayedUrl": "shop.example.com", "description": "Free shipping on all orders." }
  ],
  "peopleAlsoAsk": [{ "question": "What running shoes do podiatrists recommend?" }],
  "relatedSearches": [{ "query": "best running shoes for women" }],
  "knowledgeGraph": null,
  "aioverview": {
    "text": "The best running shoe depends on your gait and distance...",
    "markdown": "The best running shoe depends on your gait and distance...",
    "sources": [
      { "position": 1, "label": "Example Running", "title": "Best running shoes, tested", "supportingCount": 0, "url": "https://www.example.com/tested", "redirectUrl": null, "resolved": true, "domain": "example.com" }
    ],
    "complete": true
  },
  "aiModeAvailable": true,
  "pagesFetched": 1,
  "gotoResolved": 4,
  "fetchedAt": "2026-09-29T10:00:00.000Z",
  "source": "searlo.tech(google_serp)",
  "cached": false
}
```

- Organic results carry the destination in `url` (Web Search uses `link`). When Google wrapped a result in a `/goto` redirect, `redirectUrl` holds that link and `resolved` says whether `url` is the final destination. `gotoResolved` counts the links resolved for you.
- `peopleAlsoAsk` lists the questions only (`question`), not their answers. `relatedSearches` items carry `query`. `knowledgeGraph` (`title`, `type`, `description`, `attributes`, `website`) is `null` when the page has none.
- `aioverview` is `null` when Google showed no AI Overview; its `sources` are the pages it cited. `complete` is `true` when the overview was read in full, from the page as served or from the rendered page, and `false` when only part of it could be read. `pending: true` with an empty `text` marks an overview Google loads after the page; `GET /serp` then reads it from the rendered page. `rendered: true` marks an overview completed that way, which from 2026-11-01 makes the request cost 5 more (see Credits). Its `sources` come back resolved: `url` is the cited page and `redirectUrl` Google's `/goto` link. `aiModeAvailable` says whether the page offered an AI Mode tab.
- With `pages` above 1, organic results from the later pages are appended with continuing `position` values and their own `page`. Ads, videos, People Also Ask, related searches, the knowledge graph and the AI Overview come from the first page only.
- `position` counts from 1 within the response: with `page=3` the first organic result is `position` 1 on `page` 3.
- `pagesFetched` can be lower than `pages` when a later page did not render. Billing follows the pages you requested.
- Responses set `X-Cache` (`HIT`/`MISS`). Identical requests in flight at the same moment share one fetch, and each of them is billed.

### Location

Every Google endpoint applies the country and the language: `gl` sets the country and `hl` the language. `location` takes a country too, by name or two-letter code, and becomes its `gl`.

**City- and state-level location is not available yet.** In our tests (September 2026) Google no longer localizes a results page by `uule`, so a city, a state or a non-country `uule` is refused, and not charged, rather than answered with results for somewhere else:

```json
{
  "success": false,
  "code": "CITY_LOCATION_UNAVAILABLE",
  "retryable": false,
  "message": "City- and state-level location is not available yet. Country (gl) and language (hl) are applied: pass the country as gl, or as location (for example \"Germany\").",
  "location": "Austin,Texas,United States"
}
```

The same rule applies to `POST /serp/batch`, `GET /serp/features`, `GET /search/news/serp`, [SERP HTML](#serp-html), [Rank Checks](#rankings) and the `serp`, `news/serp` and `rankings/check` [async tasks](#async-tasks).

### Errors

| Status | `code` | Meaning |
|--------|----------|---------|
| `400` | `VALIDATION_ERROR` | A parameter is missing or out of range (`errors` lists which) |
| `400` | `CITY_LOCATION_UNAVAILABLE` | `location` or `uule` names a city or a state (see [Location](#full-serp)) |
| `400` | `UNKNOWN_LOCATION` | `location` is not a country we recognise; `suggestions` lists close matches |
| `429` | `AT_CAPACITY` | The endpoint is at capacity. Retry after `Retry-After`; a retry before then is refused |
| `403` | `FEATURE_NOT_AVAILABLE` | The endpoint is switched off on this deployment (`retryable: false`) |
| `422` | `QUERY_NOT_SERVED` | A query shape this endpoint does not serve: search operators (`site:`, `inurl:`, `intitle:`, `filetype:`, `before:` / `after:`), e-mail lookups and long `OR` chains (`retryable: false`) |
| `422` | `COUNTRY_UNAVAILABLE` | This `gl` cannot be served right now |
| `503` | `UPSTREAM_UNAVAILABLE` | No usable results page was available within the call (`retryable: true`). Retry after the short `Retry-After` |

Failed calls are refunded. The account [rate limit](#rate-limiting) `429` (`code` `4201` or `4202`) is separate from `AT_CAPACITY`.

### Batch Endpoint

```
POST /serp/batch
```

**Credits:** 3 per distinct keyword per page delivered. The batch reserves 3 × keywords × `pages` when it is accepted (plus 5 per keyword with `aioverview: true` from 2026-11-01) and is charged what it delivered: a keyword that failed or found nothing costs nothing. Duplicate keywords (ignoring case) are billed once. 20 keywords at 1 page cost at most 60 credits without rendered overviews. From 2026-11-01, with `aioverview: true`, a keyword whose AI Overview had to be completed by rendering the page costs 5 more (`aioverview.rendered: true` on its row). Each row's `credits` and `credits.used` say what was charged.

Up to 20 keywords run synchronously in one request, with one set of settings for all of them; `meta.max_sync_batch` reports the cap in force. For larger runs, queue `serp` tasks with [Async Tasks](#async-tasks).

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `keywords` | string[] | Yes | 1–20 keywords, each 1–400 chars |
| `gl` | string | No | Country, 2 letters (default: `us`) |
| `hl` | string | No | Language, 2–5 chars (default: `en`) |
| `location` | string | No | A country, as on `GET /serp`. A city or state refuses the whole batch with `400` `CITY_LOCATION_UNAVAILABLE` before any keyword runs (not charged). Cannot be combined with `uule` |
| `uule` | string | No | As on `GET /serp` |
| `device` | string | No | `desktop` (default). `mobile` and `tablet` are accepted and return the desktop layout, as on `GET /serp` |
| `pages` | number | No | 1–10 (default: 1) |
| `num` | number | No | 1–100 (default: 10) |
| `tbs` | string | No | As on `GET /serp` |
| `safe` | string or boolean | No | As on `GET /serp`: `active` or `off`. Every keyword in the batch is searched with it |
| `aioverview` | boolean | No | Include the AI Overview (default: **false** in a batch) |
| `googleGoto` | boolean | No | Default: true |
| `cache` | boolean | No | Default: true |

```json
{
  "schema": "serp-batch.v1",
  "results": [
    {
      "keyword": "running shoes",
      "success": true,
      "searchParameters": { "q": "running shoes", "gl": "us", "hl": "en", "page": 1, "pages": 1, "num": 10, "device": "desktop", "type": "serp" },
      "organic": [{ "position": 1, "title": "Running Shoes", "url": "https://www.example.com/running" }],
      "videos": [],
      "ads": [],
      "peopleAlsoAsk": [{ "question": "How often should you replace running shoes?" }],
      "relatedSearches": [{ "query": "running shoes sale" }],
      "knowledgeGraph": null,
      "aiModeAvailable": true,
      "pagesFetched": 1,
      "gotoResolved": 0,
      "fetchedAt": "2026-09-29T10:00:00.000Z",
      "source": "searlo.tech(google_serp)",
      "cached": false
    },
    {
      "keyword": "trail running shoes",
      "success": false,
      "status": 429,
      "code": "AT_CAPACITY",
      "retryable": true,
      "message": "Google SERP is at capacity. Retry in 60s — immediate retries will be refused."
    }
  ],
  "credits": { "used": 6 },
  "meta": { "requested": 2, "distinct": 2, "succeeded": 1, "failed": 1, "pages": 1, "gl": "us", "hl": "en", "device": "desktop", "max_sync_batch": 20 }
}
```

**A keyword that fails inside a 200 batch is not billed** (since 2026-10-05). The batch answers `200` whenever it ran; read `meta.failed` and each failed row's `retryable` before you resubmit.

### Features Endpoint

```
GET /serp/features
```

**Credits:** 3 per request (one results page). It only checks whether an AI Overview is present, so it never renders and never costs more.

Which features a query's results page shows, without the page itself. Parameters: `q` (required, 1–400 chars), `gl`, `hl`, `location` (a country) or `uule`, `device`, `safe` and `cache`, as on `GET /serp`.

```json
{
  "schema": "serp-features.v1",
  "query": "best running shoes",
  "features": ["organic", "ads", "ai_overview", "people_also_ask", "related_searches", "video", "sitelinks"],
  "counts": { "organic": 10, "ads": 3, "shopping": 0, "videos": 3, "people_also_ask": 4, "related_searches": 8, "local_pack": 0, "top_stories": 0, "image_pack": 0 },
  "organic_count": 10,
  "credits": { "used": 3 },
  "meta": { "gl": "us", "hl": "en", "device": "desktop", "cached": false, "fetchedAt": "2026-09-29T10:00:00.000Z" }
}
```

Feature names come from this list: `organic`, `ads`, `shopping`, `ai_overview`, `ai_mode`, `featured_snippet`, `people_also_ask`, `related_searches`, `knowledge_graph`, `video`, `local_pack`, `sitelinks`, `top_stories`, `image_pack`, `twitter`, `faq`. The results-page parse does not currently return shopping, local pack, top stories, image pack, Twitter, FAQ or featured-snippet blocks, so those names do not appear today and their `counts` stay 0.

### News Results Page

```
GET /search/news/serp
```

**Credits:** 2 per page requested.

Google's News tab, fetched live and parsed: cards with title, source, date, snippet and thumbnail, about a second per page. Parameters: `q` (required, 1–400 chars), `gl`, `hl`, `location` (a country) or `uule`, `page` (1–100), `pages` (1–10), `tbs` (e.g. `qdr:d` for the past day), `googleGoto` (default: true, resolves the cards' `/goto` links to the articles) and `cache`, as on `GET /serp`.

Cards come in Google's order, with continuing `position` values and the `page` each came from. For a developing story Google shows several articles as a group, and each is its own card, so a page can hold more than 10. `snippet` is empty for grouped articles, which Google shows without one. `hasNextPage` says whether Google offered a further page.

```json
{
  "success": true,
  "searchParameters": { "q": "electric vehicles", "gl": "us", "hl": "en", "page": 1, "pages": 1, "type": "news", "mode": "serp" },
  "news": [
    { "position": 1, "title": "EV sales rise again", "url": "https://news.example.com/ev-sales", "redirectUrl": null, "resolved": true, "source": "Example News", "snippet": "Sales of electric vehicles...", "date": "3 hours ago", "thumbnail": "https://news.example.com/thumb.jpg", "page": 1 }
  ],
  "hasNextPage": true,
  "pagesFetched": 1,
  "gotoResolved": 18,
  "fetchedAt": "2026-09-29T10:00:00.000Z",
  "source": "searlo.tech(google_news_serp)",
  "cached": false
}
```

## Code Examples

### cURL

```bash
# One results page; the AI Overview is included by default
curl "https://api.searlo.tech/api/v1/serp?q=best+running+shoes&gl=us&hl=en" \
  -H "x-api-key: sk_your_api_key"

# The same with SafeSearch on
curl "https://api.searlo.tech/api/v1/serp?q=best+running+shoes&safe=active" \
  -H "x-api-key: sk_your_api_key"

# Two pages of Google Germany, in German (6 credits)
curl "https://api.searlo.tech/api/v1/serp?q=kaffeemaschine&location=Germany&hl=de&pages=2" \
  -H "x-api-key: sk_your_api_key"

# Up to 20 keywords in one request
curl -X POST "https://api.searlo.tech/api/v1/serp/batch" \
  -H "x-api-key: sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "keywords": ["running shoes", "trail running shoes"], "gl": "us", "aioverview": true }'

# Only the feature list
curl "https://api.searlo.tech/api/v1/serp/features?q=best+running+shoes" \
  -H "x-api-key: sk_your_api_key"
```

### JavaScript

```javascript
const API = "https://api.searlo.tech/api/v1";
const headers = { "x-api-key": process.env.SEARLO_API_KEY };

// One full results page
const params = new URLSearchParams({ q: "best running shoes", gl: "us", hl: "en" });
const serp = await (await fetch(`${API}/serp?${params}`, { headers })).json();

serp.organic.forEach(r => console.log(r.position, r.url));
console.log(serp.peopleAlsoAsk.map(p => p.question));
if (serp.aioverview) console.log(serp.aioverview.sources.map(s => s.domain));

// A batch: one row per distinct keyword, failed rows included (not billed)
const res = await fetch(`${API}/serp/batch`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ keywords: ["running shoes", "trail running shoes"], gl: "us" }),
});
const batch = await res.json();
const retry = batch.results.filter(r => !r.success && r.retryable).map(r => r.keyword);
console.log(`${batch.credits.used} credits, ${batch.meta.failed} failed`, retry);
```

### Python

```python
import os
import requests

API = "https://api.searlo.tech/api/v1"
HEADERS = {"x-api-key": os.environ["SEARLO_API_KEY"]}

serp = requests.get(
    f"{API}/serp",
    params={"q": "best running shoes", "gl": "us", "hl": "en", "pages": 1},
    headers=HEADERS,
    timeout=120,
).json()

for r in serp["organic"]:
    print(r["position"], r["url"])
if serp.get("aioverview"):
    print([s["domain"] for s in serp["aioverview"]["sources"]])

batch = requests.post(
    f"{API}/serp/batch",
    json={"keywords": ["running shoes", "trail running shoes"], "gl": "us"},
    headers=HEADERS,
    timeout=300,
).json()
print(batch["meta"]["succeeded"], "ok,", batch["meta"]["failed"], "failed,", batch["credits"]["used"], "credits")
```


---
*From https://searlo.tech/docs — see full docs at https://searlo.tech/docs/full.md*