API reference

Submit a search

Start a crawl. Returns a job you poll for results. Asynchronous by default; optionally blocking.

Endpoint

POST/v1/search

Send a JSON body with at least a keyword. The response carries a job_id and a status; fetch the parsed results from GET /v1/search/{job_id}.

request
curl -X POST https://serp-api.hoangha.shop/v1/search \
  -H "X-API-Key: sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "best running shoes",
    "location": "Hanoi, Vietnam",
    "device": "desktop",
    "async": true
  }'
response
{ "job_id": "9f3c1e2a-...", "status": "queued", "cached": false }

Core parameters

FieldTypeDescription
keywordrequiredstringThe search query. 1–400 characters.
locationstringLocality to geo-target, e.g. "Hanoi, Vietnam". Resolves gl, hl, and the UULE. Preferred over setting gl/hl by hand.
devicestring"desktop" (default), "mobile", or "tablet". Each presents a coherent user-agent and client hints.
asyncbooleantrue (default) returns a job_id immediately. false blocks until the crawl finishes or the sync timeout elapses.
stop_domainstringRank-check shortcut: stop paginating once this domain appears (host-matched). Saves egress when you only need one domain’s position.

Geo targeting

Prefer location — it resolves the correct country, language, and UULE from a single locality name. The overrides below are for advanced callers who want explicit control.

FieldTypeDescription
glstringCountry code override, e.g. "vn", "us", "gb". Overrides the country derived from location.
hlstringInterface-language override, e.g. "vi", "en". Overrides the language derived from location.
uulestringPre-encoded UULE passthrough for precise geo. Overrides the value derived from location; gl/hl still resolve normally.
google_domainstringSearch host, e.g. "google.co.uk". Equivalent to SerpApi google_domain / DataForSEO se_domain.
latnumberLatitude to search from. Requires lon. Encoded into the coordinate form of the UULE.
lonnumberLongitude to search from. Requires lat.
radiusintegerMetres to bias results around lat/lon (1–1000). Omit to treat the coordinates as exact.
location, uule and lat/lon all answer “where is this search from”, and Google honours exactly one. Sending more than one returns 422 rather than silently picking a winner — a SERP for the wrong place is indistinguishable from a correct one once you have it.
Locales roll out in stages. A request for a country that is not yet enabled returns 403 with detail locale '{gl}' not enabled.

What to capture

The AI Overview is captured by default and fetches no extra result page, since it reuses the same page-1 load as the organic results. It is not free, though: it adds a render wait, and on a fast-absent AI Overview the crawler reloads the SERP once to re-roll it. Set capture_aio to false to skip both: the crawl returns sooner and the response carries no ai_overview.

FieldTypeDescription
capture_aiobooleanCapture the AI Overview. Default true. False skips the AI Overview render wait for a faster crawl and returns no ai_overview. Part of the cache key, so AIO-on and AIO-off results never mix.
capture_featuresbooleanParse the rich SERP for feature blocks (featured snippet, PAA, packs, etc). Defaults to whatever capture_aio resolves to, reusing the same page-1 load (zero extra requests). Set it true explicitly to keep features while capture_aio is false.
pagesinteger1–10. How many result pages to CRAWL (10 results each). Unset crawls the configured depth, normally the full ~100 results. This is the one knob that trades coverage for latency, and it is deliberately an explicit opt-in: pages=1 is a single navigation and by far the fastest search, since full depth costs roughly ten times the page requests. Part of the cache key — a shallow crawl is never served to a caller who asked for full depth, though a cached full crawl will serve a shallow request.
modestringCrawl strategy for the organic pages (the AI Overview is always a browser load). null (default) uses the configured strategy ("browserfetch2"). Selectable: "browserfetch2" (default; N-tab pair variant, crawls matching-locale keywords together in one browser), "browserfetch" (in-browser fetch; holds the browser slot for the whole crawl), "wreq" (Rust HTTP engine reusing browser cookies), "cffi" (curl_cffi HTTP client), "primp" (Rust reqwest HTTP client). The three HTTP-client modes are A/B baselines and measure far slower on the current pool. "browser" is an internal escalation fallback only, not user-selectable.

Google query knobs

Optional parameters that mirror Google’s own search filters (SerpApi / DataForSEO parity). Each is part of the result cache key, so variants cache independently.

FieldTypeDescription
tbsstringAdvanced / time filter, e.g. "qdr:d" (past day), "qdr:w" (past week).
safestringSafeSearch: "active" or "off".
nfprbooleanWhen true, exclude auto-corrected (spelling-fixed) results. Default false.
filterbooleanThe omitted/duplicate-results filter. false disables it (show all). null (default) uses Google’s default.
lrstringRestrict result languages, e.g. "lang_en".
crstringRestrict result countries, e.g. "countryUS".
tbmstringGoogle vertical: "nws" (news), "isch" (images), "vid" (video), "shop" (shopping). Replaces the default web filter; non-web verticals return raw HTML plus a best-effort parse.
color_schemestring"light" or "dark" — forces the SERP colour scheme (Google’s cs parameter).
no_cachebooleanCrawl fresh instead of serving a recent identical result. The fresh result is still cached for later callers, so this costs one crawl, not every future one.

Entity and rendering identifiers

Opaque values Google issues, which you copy from a SERP or a local result to target one entity or reproduce one exact page. They are escaped and forwarded verbatim — we never interpret them — and each is part of the cache key.

FieldTypeDescription
ludocidstringGoogle CID of a place from local results — targets that one business.
lsigstringForces the knowledge-graph map view for a local entity.
kgmidstringKnowledge Graph entity id, e.g. "/m/0212jm".
sistringCached search parameters — reproduces a SERP whose parameters Google encrypted.
ibpstringElement rendering / expansion control, e.g. "htl;jobs".
udsstringGoogle-issued filter string, copied from a SERP filter chip.

Pagination

Crawl depth is chosen once, at submit, with pages — it defaults to the full ~100 results because the raw SERP we store and serve has to be complete. start and num are separate: they are query parameters on the read, windowing an already-finished crawl. Pass them to GET /v1/search/{job_id}.

FieldTypeDescription
startintegerResult offset on GET /v1/search/{job_id} — 0 returns from result 1, 10 from result 11. Every offset is served from the same stored SERP, so paging costs nothing and never triggers a second crawl.
numintegerPage size on GET /v1/search/{job_id} — the maximum number of organic results to return, applied after start. Like start, it windows the stored crawl and is never sent to Google.
position is never renumbered: at start=10 the first row is still position 11. And completeness keeps describing the crawl, not the window you asked for.

Async vs sync

Async (default)

The request returns immediately with status: "queued". The crawl runs on the worker fleet; you poll GET /v1/search/{job_id} for the outcome. This is the right choice for batch work and high volume.

Sync

With async: false, the request runs the crawl off-thread and blocks up to the server’s sync timeout, then returns the job’s current state. If the timeout elapses first, you get a non-terminal status and the crawl keeps running in the background — you can still fetch it later by job_id.

Sync and async crawl identically

Sync does not run the crawl on the request thread — it enqueues to the same worker fleet as async and polls the job row, so the two produce the same result and honour the same settings. Sync only bounds how long the API waits before handing back whatever state has settled. Choose it for latency, not for a different crawl.

Caching & idempotency

Results are cached for a short window, keyed on keyword + location + device + mode + UULE + the query knobs above. A repeat request within that window returns cached: true and does not consume quota. Change any keyed parameter to force a fresh crawl.