API reference
Submit a search
Start a crawl. Returns a job you poll for results. Asynchronous by default; optionally blocking.
Endpoint
/v1/searchSend 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}.
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
}'{ "job_id": "9f3c1e2a-...", "status": "queued", "cached": false }Core parameters
| Field | Type | Description |
|---|---|---|
| keywordrequired | string | The search query. 1–400 characters. |
| location | string | Locality to geo-target, e.g. "Hanoi, Vietnam". Resolves gl, hl, and the UULE. Preferred over setting gl/hl by hand. |
| device | string | "desktop" (default), "mobile", or "tablet". Each presents a coherent user-agent and client hints. |
| async | boolean | true (default) returns a job_id immediately. false blocks until the crawl finishes or the sync timeout elapses. |
| stop_domain | string | Rank-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.
| Field | Type | Description |
|---|---|---|
| gl | string | Country code override, e.g. "vn", "us", "gb". Overrides the country derived from location. |
| hl | string | Interface-language override, e.g. "vi", "en". Overrides the language derived from location. |
| uule | string | Pre-encoded UULE passthrough for precise geo. Overrides the value derived from location; gl/hl still resolve normally. |
| google_domain | string | Search host, e.g. "google.co.uk". Equivalent to SerpApi google_domain / DataForSEO se_domain. |
| lat | number | Latitude to search from. Requires lon. Encoded into the coordinate form of the UULE. |
| lon | number | Longitude to search from. Requires lat. |
| radius | integer | Metres 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.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.
| Field | Type | Description |
|---|---|---|
| capture_aio | boolean | Capture 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_features | boolean | Parse 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. |
| pages | integer | 1–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. |
| mode | string | Crawl 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.
| Field | Type | Description |
|---|---|---|
| tbs | string | Advanced / time filter, e.g. "qdr:d" (past day), "qdr:w" (past week). |
| safe | string | SafeSearch: "active" or "off". |
| nfpr | boolean | When true, exclude auto-corrected (spelling-fixed) results. Default false. |
| filter | boolean | The omitted/duplicate-results filter. false disables it (show all). null (default) uses Google’s default. |
| lr | string | Restrict result languages, e.g. "lang_en". |
| cr | string | Restrict result countries, e.g. "countryUS". |
| tbm | string | Google 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_scheme | string | "light" or "dark" — forces the SERP colour scheme (Google’s cs parameter). |
| no_cache | boolean | Crawl 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.
| Field | Type | Description |
|---|---|---|
| ludocid | string | Google CID of a place from local results — targets that one business. |
| lsig | string | Forces the knowledge-graph map view for a local entity. |
| kgmid | string | Knowledge Graph entity id, e.g. "/m/0212jm". |
| si | string | Cached search parameters — reproduces a SERP whose parameters Google encrypted. |
| ibp | string | Element rendering / expansion control, e.g. "htl;jobs". |
| uds | string | Google-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}.
| Field | Type | Description |
|---|---|---|
| start | integer | Result 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. |
| num | integer | Page 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
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.