SERP & rankings
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 returns organic results only, for 1 credit, in about a second.
Endpoints#
- GET
/serp - POST
/serp/batch - GET
/serp/features - GET
/search/news/serp
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.
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); 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 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 with js=true |
Response#
{ "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 useslink). When Google wrapped a result in a/gotoredirect,redirectUrlholds that link andresolvedsays whetherurlis the final destination.gotoResolvedcounts the links resolved for you. peopleAlsoAsklists the questions only (question), not their answers.relatedSearchesitems carryquery.knowledgeGraph(title,type,description,attributes,website) isnullwhen the page has none.aioverviewisnullwhen Google showed no AI Overview; itssourcesare the pages it cited.completeistruewhen the overview was read in full, from the page as served or from the rendered page, andfalsewhen only part of it could be read.pending: truewith an emptytextmarks an overview Google loads after the page;GET /serpthen reads it from the rendered page.rendered: truemarks an overview completed that way, which from 2026-11-01 makes the request cost 5 more (see Credits). Itssourcescome back resolved:urlis the cited page andredirectUrlGoogle's/gotolink.aiModeAvailablesays whether the page offered an AI Mode tab.- With
pagesabove 1, organic results from the later pages are appended with continuingpositionvalues and their ownpage. Ads, videos, People Also Ask, related searches, the knowledge graph and the AI Overview come from the first page only. positioncounts from 1 within the response: withpage=3the first organic result isposition1 onpage3.pagesFetchedcan be lower thanpageswhen 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:
{ "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, Rank Checks and the serp, news/serp and rankings/check 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) |
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 429 (code 4201 or 4202) is separate from AT_CAPACITY.
Batch Endpoint#
https://api.searlo.tech/api/v1/serp/batchCredits: 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.
| 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 |
{ "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#
https://api.searlo.tech/api/v1/serp/featuresCredits: 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.
{ "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#
https://api.searlo.tech/api/v1/search/news/serpCredits: 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.
{ "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}Examples
# One results page; the AI Overview is included by defaultcurl "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 oncurl "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 requestcurl -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 listcurl "https://api.searlo.tech/api/v1/serp/features?q=best+running+shoes" \ -H "x-api-key: sk_your_api_key"const API = "https://api.searlo.tech/api/v1";const headers = { "x-api-key": process.env.SEARLO_API_KEY }; // One full results pageconst 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);import osimport 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")Stuck? Write to [email protected], or check live status.