Integrations
SerpApi drop-in
Already using serpapi.com? Point your existing client at this host and keep your parameters, your endpoints, and your response parsing.
Change the host, change nothing else
We serve serpapi.com’s own paths, parameter names, authentication style and response envelope. Both official Python clients keep their base URL in a class attribute, so switching is one line.
import serpapi
serpapi.HTTPClient.BASE_DOMAIN = "https://serp-api.hoangha.shop"
client = serpapi.Client(api_key="sk_live_...")
results = client.search({"engine": "google", "q": "coffee", "location": "Hanoi"})
print(results["organic_results"][0]["link"])from serpapi import GoogleSearch
GoogleSearch.BACKEND = "https://serp-api.hoangha.shop"
search = GoogleSearch({"q": "coffee", "gl": "vn", "api_key": "sk_live_..."})
print(search.get_dict()["organic_results"])curl "https://serp-api.hoangha.shop/search?engine=google&q=coffee&gl=vn&api_key=sk_live_..."Endpoints
/search/searches/{search_id}/account/locations.json| Field | Type | Description |
|---|---|---|
| /search | endpoint | Run a search. Blocking by default; add async=true to get a job id back and poll. Also served at /search.json. Add output=html for the raw SERP HTML. |
| /searches/{id} | endpoint | Fetch a past search. Accepts a bare id, id.json, or id.html — the two Python clients disagree about whether the extension belongs in the path, so both work. |
| /account | endpoint | Key tier, monthly quota, usage this month, and searches remaining. Also served at /account.json. |
| /locations.json | endpoint | Search the location values that are valid in the location parameter. Unauthenticated, as theirs is. |
Authentication is ?api_key=, exactly as on serpapi.com. An X-API-Key header also works if you prefer it. Failures come back as a bare {"error": "..."}, which is the shape their clients raise on.
Parameters
Every Google Search parameter serpapi.com accepts is accepted here under the same name: q, location, uule, lat, lon, radius, google_domain, gl, hl, cr, lr, tbs, tbm, safe, nfpr, filter, ludocid, lsig, kgmid, si, ibp, uds, color_scheme, start, num, device, no_cache, async and output.
Unrecognised parameters are ignored rather than rejected, so a client library that appends its own bookkeeping field will not start failing.
Our extensions
| Field | Type | Description |
|---|---|---|
| pages | integer | 1–10. How many result pages to crawl, overriding the depth derived from num/start. Unset uses the derived depth. pages=1 is a single navigation and the fastest search available. |
| capture_aio | boolean | Include the AI Overview inline. Defaults to on, which is richer than serpapi.com (they defer it to a second billable call via page_token). Set false to skip the render wait for a materially faster search. |
Response
The envelope carries the same top-level blocks: search_metadata, search_parameters, search_information, organic_results, pagination, serpapi_pagination, plus the SERP feature blocks (answer_box, related_questions, knowledge_graph, related_searches, and others) when present.
{
"search_metadata": {
"id": "9f3c1a…",
"status": "Success",
"json_endpoint": "https://serp-api.hoangha.shop/searches/9f3c1a….json",
"created_at": "2026-08-18 04:00:00 UTC",
"processed_at": "2026-08-18 04:00:41 UTC",
"google_url": "https://www.google.com/search?q=coffee&gl=vn&hl=vi&pws=0",
"raw_html_file": "https://serp-api.hoangha.shop/searches/9f3c1a….html",
"total_time_taken": 41.0
},
"search_parameters": { "engine": "google", "q": "coffee", "gl": "vn", "hl": "vi", "device": "desktop" },
"search_information": { "query_displayed": "coffee", "total_results": 1240000, "organic_results_state": "Results for exact spelling" },
"organic_results": [ { "position": 1, "title": "…", "link": "https://…", "snippet": "…" } ],
"pagination": { "current": 1, "next": "…", "other_pages": { "2": "…" } },
"serpapi_pagination": { "current": 1, "next": "…", "next_link": "…", "other_pages": { "2": "…" } }
}ai_overview is also present, and is captured by default rather than being a separate paid add-on.
Four differences worth knowing
1. A search takes seconds, not milliseconds
We crawl Google live from real residential addresses, so a fresh search is slower than serpapi.com’s cache-weighted average. A blocking call waits up to the server’s sync timeout; if the crawl has not finished by then you get SerpApi’s own async shape back — status: "Processing" plus a json_endpoint — and poll it. Nothing is lost when that happens: the crawl keeps running and the result lands.
num=10 search is one page; asking for num=100 is ten pages and takes proportionally longer. For batch work send async=true and poll /searches/{id} rather than blocking on each call.2. num and start decide how much we crawl
On serpapi.com one call returns one page, and num defaults to 10. We match that: we crawl only the pages your window touches. num=10 is a single navigation — the fastest answer this service can give — while num=100 collects the full ten pages, and start=90 reaches the tenth. Neither is ever sent to Google as a URL parameter; they select how many pages we fetch, and then window the results.
position is never renumbered, so at start=10 the first row is still position 11. If a recent full-depth crawl of the same query is still cached, a small num is served straight from it and costs no crawl at all.
pages (1–10) sets the crawl depth directly and overrides the window, and capture_aio=false drops the AI Overview render wait. num=10&capture_aio=false is the lowest-latency search available.3. Geo sources are mutually exclusive
location, uule and lat/lon all answer “where is this search from”, and Google honours exactly one. Sending more than one is a 400 rather than a silent pick, because a SERP for the wrong place looks exactly like a correct one once you have it.
4. Google-only, and a few fields we will not invent
engine must be google; any other value is refused by name. json_restrictor is accepted and ignored — you get the full payload, which is a superset of what you asked for. On /account, the plan-price fields report 0 because billing lives outside this API, and /locations.json omits Google’s internal ids and reach rather than fabricating them.
Or use the native API
The compatibility surface exists so you can migrate without touching code. Our own /v1 API is a better fit for new work: it is async-first, exposes the native output schema with the completeness ratio and per-crawl diagnostics, and has no translation layer in front of it.