> Searlo API Docs — SERP HTML (raw & rendered)
> Source: https://searlo.tech/docs#serp-html
> Full docs: https://searlo.tech/docs/full.md

# SERP HTML (raw & rendered)

Google's results page as HTML, for callers that parse the page themselves. Two ways, one endpoint:

- **Without JavaScript** (default): the full results page exactly as Google's server sent it. Nothing is extracted, rewritten or re-serialised: result links are Google's own `/goto` redirects, as on the page. Typically 2–3 seconds, about 1 MB of HTML.
- **With JavaScript** (`js=true`): the same page rendered, with the **AI Overview opened out**: finished streaming, with its **Show more** and **Show all** sections expanded in the DOM that comes back. Typically 8–15 seconds. Images, fonts and media are not downloaded; the HTML references them.

Both work for **desktop** and for **iOS** (`device=ios`): the page Google serves to Safari on an iPhone.

### Endpoint

```
GET /serp/html
```

**Credits:** 2 per page; 8 per page with `js=true`. Failed calls are refunded. Never cached: each call fetches the page at request time.

### 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`) |
| `page` | number | No | Results page (1–10, default: 1) |
| `tbs` | string | No | Google time or sort filter, max 40 chars (e.g. `qdr:w`) |
| `safe` | string | No | `active` turns SafeSearch on; `off` |
| `device` | string | No | `desktop` (default) or `ios` |
| `js` | boolean | No | `true` returns the rendered page with the AI Overview opened out (default: `false`) |
| `expandAio` | boolean | No | With `js=true`: return the AI Overview with its **Show more** and **Show all** sections expanded (default: `true`). `false` returns the rendered page as first shown |
| `format` | string | No | `json` (default) wraps the page in a JSON envelope; `raw` answers with the page itself as `text/html` |
| `location` | string | No | A country, by name or two-letter code (e.g. `Germany`, `DE`), applied as `gl`. A city or state returns `400` `CITY_LOCATION_UNAVAILABLE` and is not charged (see [Location](#full-serp)). Cannot be combined with `uule` |
| `uule` | string | No | A country's `uule` is applied as its `gl`; any other token returns `400` `CITY_LOCATION_UNAVAILABLE` |

One call is one results page: `page=2` is results 11–20. Google serves about ten organic results a page and has not honoured a larger `num` since 2025, so there is no `num` here. Two identical calls are two fetches, and two pages.

### Raw or rendered?

| | Raw (default) | Rendered (`js=true`) |
|--|--|--|
| Credits | 2 | 8 |
| Typical time | 2–3 s (p95 about 7 s) | 8–15 s |
| HTML | about 1 MB | 1–2 MB |
| What it is | The bytes Google's server sent, before any JavaScript ran | The DOM after Google's JavaScript ran, with the AI Overview opened out |
| Organic results, ads, People Also Ask questions, related searches, knowledge panel | Yes | Yes |
| AI Overview | Its first part only: Google streams the rest in after the page loads (in our measurements a median of 39% of the full text, and 1–4 of its sources) | In full, every source included (**Show more** and **Show all** pressed) |
| Links | Google's `/goto` redirects, as served | As on the rendered page |
| iOS (`device=ios`) | Yes | Yes |

Raw is the choice for rank and SERP-feature data at volume. Rendered is for the AI Overview's full text and sources, or the page as a person sees it. For the overview as JSON rather than HTML, [Full SERP](#full-serp) reads it in full for 3 credits (from 2026-11-01, 8 when it has to render the page to complete the overview).

### Response (`format=json`)

```json
{
  "success": true,
  "searchParameters": { "q": "how does compound interest work", "type": "rendered", "gl": "us", "hl": "en", "page": 1, "device": "desktop" },
  "html": "<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/SearchResultsPage\" lang=\"en\">…",
  "url": "https://www.google.com/search?q=how+does+compound+interest+work&hl=en&gl=us",
  "empty": false,
  "rendered": true,
  "aio": { "present": true, "expanded": true, "sourcesOpened": true, "showAll": "pressed", "sourcesBefore": 7, "sources": 7, "sourcesVisible": 11 },
  "aiOverview": {
    "text": "Compound interest is interest earned on both the principal and the interest already added to it...",
    "markdown": "Compound interest is interest earned on both the principal and the interest already added to it...",
    "sources": [{ "position": 1, "label": "Investopedia", "url": "https://www.google.com/goto?url=…", "resolved": false }]
  },
  "bytes": 2089123,
  "fetchedAt": "2026-09-29T18:39:40.000Z",
  "source": "searlo.tech(google_serp_rendered)",
  "credits": { "used": 8 }
}
```

| Field | Description |
|-------|-------------|
| `html` | The page. Without `js`: the bytes Google's server sent. With `js`: the DOM after the page's JavaScript ran and the AI Overview was opened out |
| `url` | The results page's URL |
| `empty` | `true` when Google answered with a results page that has no results (it is still the page, and still returned) |
| `rendered` | Whether JavaScript ran (`js=true`) |
| `aio` | With `js=true`, what happened to the AI Overview: `present` (Google showed one), `expanded` (Show more took), `sourcesOpened` (every source is on show: Show all took, or there was nothing left to open), `showAll` (what the Show all step found: `pressed`; `absent`, no Show all to press; `complete`, every source already on screen; `failed`, the press took nowhere with sources still hidden), `sources` (unique source links on the page), `sourcesVisible` (source links on screen) |
| `aiOverview` | With `js=true` on desktop: the overview parsed from the captured page (`text`, `markdown`, `sources`). `null` on iOS: the mobile layout is in the `html` only |
| `bytes` | Size of `html` in bytes (UTF-8) |

### Response (`format=raw`)

The body is the page itself, `Content-Type: text/html; charset=utf-8`. The metadata moves to headers:

| Header | Value |
|--------|-------|
| `X-Searlo-Url` | The results page's URL |
| `X-Searlo-Fetched-At` | When it was fetched (ISO 8601) |
| `X-Searlo-Empty` | `true` for a results page with no results |
| `X-Searlo-Rendered` | `true` when JavaScript ran |
| `X-Searlo-Aio` | With `js=true`: `present=…; expanded=…; sources-opened=…; sources=…; show-all=…` |
| `X-Credits-Used` | Credits this call took |

### AI Overview notes

- Google decides whether an AI Overview appears, per query and per request. `aio.present` tells you; the page is billed either way.
- **Desktop:** the overview renders in two parts once opened; all of its sources are in the returned HTML, and **Show all** makes them visible. A short sources list is shown in full without one (`showAll` `absent` or `complete`).
- **iOS:** the mobile overview has no **Show all** control (`showAll: "absent"`, `sourcesOpened: true`); its sources are cards inside the overview, all in the HTML. Its **Show more** lifts a height limit rather than adding content, so on iOS `aio.expanded` means the tap went through. The overview's full text is in the HTML once it has finished streaming, which is what the render waits for.
- An AI Overview is non-deterministic: the same query can return a different overview, with different sources, from one request to the next.

### iOS

`device=ios` returns the mobile layout Google serves to Safari on an iPhone, from an emulated iPhone rather than a physical device.

### Latency, capacity and timeouts

Measured through the public API (Cloudflare, our edge, the SERP hosts) in September 2026:

| | p50 | p95 | Success |
|--|--|--|--|
| Raw, 6 concurrent | 3.1 s | 7.6 s | 99.3% of 302 calls |
| Rendered, 3 concurrent | 9.2 s | 11.9 s | 100% of 60 calls |

Pages are fetched on dedicated SERP hosts. One host sustains at least 5 raw pages a second (97% or better at 16 concurrent requests, the most we have driven it) and about 0.7 rendered pages a second (rendering is CPU-bound), and capacity grows by adding hosts. For sustained volume, tell us your daily volume and peak rate per mode, and capacity is reserved for your key before you ramp.

Set client timeouts of **30 s** for raw pages and **90 s** for rendered ones. The server gives up at about 92 s and answers either way: a call that runs out of time is a `503` and is not charged.

### Retries

- `429` `BUSY`: the endpoint is at capacity (with `js=true`, rendering capacity). Wait `Retry-After` seconds (1–10) and retry.
- `503` with `retryable: true` (`UPSTREAM_UNAVAILABLE`): no usable page was available within the call. Retry with backoff, for example after 2, 5 and 10 seconds.
- `400` and `422`: retrying the same request gives the same answer.
- A failed call is never charged, so a retry cannot bill twice: you pay once, for the call that returns the page.

### Errors

| Status | `code` | Meaning | Retry |
|--------|----------|---------|-------|
| `400` | `VALIDATION_ERROR` | Invalid parameters, or a `device` other than `desktop` / `ios` | No |
| `400` | `CITY_LOCATION_UNAVAILABLE` | `location` or `uule` names a city or a state | No |
| `400` | `UNKNOWN_LOCATION` | `location` is not a country we recognise (`suggestions` lists close matches) | No |
| `402` | `INSUFFICIENT_CREDITS` | Not enough credits for the call | After a top-up |
| `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 | No |
| `422` | `COUNTRY_UNAVAILABLE` | This `gl` cannot be served right now | Later |
| `429` | `BUSY` | At capacity (with `js=true`, rendering capacity) | After `Retry-After` |
| `429` | `4201` / `4202` | The account or API-key [rate limit](#rate-limiting) | After `Retry-After` |
| `403` | `FEATURE_NOT_AVAILABLE` | Switched off on this deployment (`retryable: false`) | No |
| `503` | `UPSTREAM_UNAVAILABLE` | No usable page was available within the call (`retryable: true`) | Yes, with backoff |

None of them is charged. A failure answers JSON even with `format=raw`:

```json
{
  "success": false,
  "code": "QUERY_NOT_SERVED",
  "retryable": false,
  "message": "This query shape is not served as raw HTML: search operators (site:, inurl:, intitle:, filetype:, before:/after:), e-mail lookups and long OR chains."
}
```

### Going to production

- Use `format=raw` when you store or parse the page: it arrives as `text/html` with no JSON escaping, and the metadata is in the `X-Searlo-*` headers.
- Send `Accept-Encoding: gzip`. A raw page is about 1 MB of HTML and compresses to a fraction of that.
- Honour `Retry-After` on every `429`, and retry `503`s marked `retryable: true` with backoff.
- Use the timeouts above: 30 s raw, 90 s rendered.
- Track spend from `X-Credits-Used` (`credits.used` in JSON) and `X-Credits-Remaining`.
- Read [`GET /search/capabilities`](#account) for the endpoint's live state and its success rate over the last two hours.
- A key's default caps are 20,000 requests a minute and 400,000 a day ([Rate Limiting](#rate-limiting)). Ask us for custom limits and reserved capacity before a large ramp.

## Code Examples

### cURL

```bash
# The page as Google sent it (2 credits)
curl "https://api.searlo.tech/api/v1/serp/html?q=best+running+shoes&gl=us&hl=en" \
  -H "x-api-key: sk_your_api_key"

# The same, as the HTML file itself
curl "https://api.searlo.tech/api/v1/serp/html?q=best+running+shoes&format=raw" \
  -H "x-api-key: sk_your_api_key" -o serp.html

# Rendered, AI Overview opened out (8 credits)
curl "https://api.searlo.tech/api/v1/serp/html?q=how+does+compound+interest+work&js=true" \
  -H "x-api-key: sk_your_api_key"

# What an iPhone gets, rendered
curl "https://api.searlo.tech/api/v1/serp/html?q=how+does+compound+interest+work&device=ios&js=true&format=raw" \
  -H "x-api-key: sk_your_api_key" -o serp-ios.html
```

### JavaScript

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

// Rendered, AI Overview opened out
const params = new URLSearchParams({ q: "how does compound interest work", js: "true" });
const res = await fetch(API + "/serp/html?" + params, { headers });
const page = await res.json();
if (!page.success) throw new Error(page.message);

console.log(page.aio); // { present, expanded, sourcesOpened, showAll, sources, ... }
await import("node:fs").then(fs => fs.writeFileSync("serp.html", page.html));
```

### Python

```python
import os
import requests

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

# The page an iPhone gets, rendered, as the HTML file itself
r = requests.get(
    f"{API}/serp/html",
    params={"q": "how does compound interest work", "device": "ios", "js": "true", "format": "raw"},
    headers=HEADERS,
    timeout=90,
)
r.raise_for_status()
print(r.headers.get("X-Searlo-Aio"))
with open("serp-ios.html", "w", encoding="utf-8") as f:
    f.write(r.text)
```


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