SERP & rankings

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#

GEThttps://api.searlo.tech/api/v1/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#

ParameterTypeRequiredDescription
qstringYesSearch query (1–400 chars)
glstringNoCountry, 2 letters (default: us)
hlstringNoLanguage, 2–5 chars (default: en)
pagenumberNoResults page (1–10, default: 1)
tbsstringNoGoogle time or sort filter, max 40 chars (e.g. qdr:w)
safestringNoactive turns SafeSearch on; off
devicestringNodesktop (default) or ios
jsbooleanNotrue returns the rendered page with the AI Overview opened out (default: false)
expandAiobooleanNoWith 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
formatstringNojson (default) wraps the page in a JSON envelope; raw answers with the page itself as text/html
locationstringNoA 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). Cannot be combined with uule
uulestringNoA 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)
Credits28
Typical time2–3 s (p95 about 7 s)8–15 s
HTMLabout 1 MB1–2 MB
What it isThe bytes Google's server sent, before any JavaScript ranThe DOM after Google's JavaScript ran, with the AI Overview opened out
Organic results, ads, People Also Ask questions, related searches, knowledge panelYesYes
AI OverviewIts 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)
LinksGoogle's /goto redirects, as servedAs on the rendered page
iOS (device=ios)YesYes

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 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 }}
FieldDescription
htmlThe 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
urlThe results page's URL
emptytrue when Google answered with a results page that has no results (it is still the page, and still returned)
renderedWhether JavaScript ran (js=true)
aioWith 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)
aiOverviewWith 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
bytesSize 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:

HeaderValue
X-Searlo-UrlThe results page's URL
X-Searlo-Fetched-AtWhen it was fetched (ISO 8601)
X-Searlo-Emptytrue for a results page with no results
X-Searlo-Renderedtrue when JavaScript ran
X-Searlo-AioWith js=true: present=…; expanded=…; sources-opened=…; sources=…; show-all=…
X-Credits-UsedCredits 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:

p50p95Success
Raw, 6 concurrent3.1 s7.6 s99.3% of 302 calls
Rendered, 3 concurrent9.2 s11.9 s100% 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#

StatuscodeMeaningRetry
400VALIDATION_ERRORInvalid parameters, or a device other than desktop / iosNo
400CITY_LOCATION_UNAVAILABLElocation or uule names a city or a stateNo
400UNKNOWN_LOCATIONlocation is not a country we recognise (suggestions lists close matches)No
402INSUFFICIENT_CREDITSNot enough credits for the callAfter a top-up
422QUERY_NOT_SERVEDA query shape this endpoint does not serve: search operators (site:, inurl:, intitle:, filetype:, before: / after:), e-mail lookups and long OR chainsNo
422COUNTRY_UNAVAILABLEThis gl cannot be served right nowLater
429BUSYAt capacity (with js=true, rendering capacity)After Retry-After
4294201 / 4202The account or API-key rate limitAfter Retry-After
403FEATURE_NOT_AVAILABLESwitched off on this deployment (retryable: false)No
503UPSTREAM_UNAVAILABLENo 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 503s 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` 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). Ask us for custom limits and reserved capacity before a large ramp.

Examples

# 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 itselfcurl "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, renderedcurl "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
const API = "https://api.searlo.tech/api/v1";const headers = { "x-api-key": process.env.SEARLO_API_KEY }; // Rendered, AI Overview opened outconst 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));
import osimport 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 itselfr = 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)

Stuck? Write to [email protected], or check live status.