API reference
Retrieve results
Fetch a job by id in the output format you want. Poll while it runs; read structured results when it finishes.
Endpoint
GET
/v1/search/{job_id}?output=serpapi| Field | Type | Description |
|---|---|---|
| job_idrequired | string | The id returned by POST /v1/search (path parameter). |
| output | string | "html" (default), "json", or "serpapi". Controls the response shape. |
| num | integer | Maximum organic results to return, applied after start. Windows the stored crawl; never sent to Google. To crawl fewer pages in the first place (and get a much faster search), use the pages parameter on POST /v1/search. |
| start | integer | Result offset — 0 (default) returns from result 1, 10 from result 11. Crawls are always full depth, so every offset is served from the same stored SERP: paging costs nothing and never triggers a second crawl. Positions are not renumbered, and completeness keeps describing the crawl rather than the window. |
Job lifecycle
A job moves through these statuses:
| Field | Type | Description |
|---|---|---|
| queued | non-terminal | Accepted and waiting for a worker. |
| running | non-terminal | The crawl is in progress. |
| done | terminal | Completed; results are available. |
| partial | terminal | Completed with fewer than the full 100 results (blocks or an exhausted keyword). Results are still returned; check completeness. |
| failed | terminal | The crawl could not complete. See the meta.error field. |
HTTP status while polling
For
output=json or serpapi, a non-terminal job returns 202 Accepted with a small body like { "job_id": "...", "status": "running" }. Poll until you get 200.Polling pattern
Poll every 1 to 2 seconds until the status is terminal.
node.js
async function getResults(jobId, apiKey) {
const url = `https://serp-api.hoangha.shop/v1/search/${jobId}?output=serpapi`
for (;;) {
const res = await fetch(url, { headers: { 'X-API-Key': apiKey } })
if (res.status === 200) return res.json() // done / partial
if (res.status === 202) { // still running
await new Promise((r) => setTimeout(r, 1500))
continue
}
throw new Error(`unexpected status ${res.status}`)
}
}Output formats
The output query parameter selects the response shape.
| Field | Type | Description |
|---|---|---|
| html | default | Returns job metadata plus a presigned URL to the raw assembled SERP HTML. Re-parseable later; the source of truth for every other format. |
| json | native | Our own stable schema: search_metadata, search_parameters, organic_results, ai_overview, serp_features. |
| serpapi | compatible | SerpApi-compatible field names (organic_results[].link, answer_box, related_questions, ai_overview.references, …). Easiest drop-in migration. |
html (default)
output=html
{
"job_id": "9f3c1e2a-...",
"status": "done",
"cached": false,
"meta": {
"keyword": "best running shoes",
"gl": "vn", "hl": "vi", "device": "desktop",
"collected": 100, "target": 100,
"aio_status": "present", "aio_citations": 7,
"total_results": 48200000, "duration_ms": 8410, "mode": "browserfetch2"
},
"html_url": "https://storage.../9f3c1e2a.html?X-Amz-Signature=..."
}json (native)
output=json
{
"search_metadata": {
"job_id": "9f3c1e2a-...", "status": "done",
"parser_version": "7", "collected": 100, "target": 100,
"total_results": 48200000, "completeness": 1.0, "duration_ms": 8410
},
"search_parameters": { "q": "best running shoes", "location": "Hanoi, Vietnam", "gl": "vn", "hl": "vi", "device": "desktop" },
"organic_results": [
{ "position": 1, "title": "...", "url": "https://...", "displayed_url": "...", "snippet": "...", "favicon": "...", "domain_info": { ... }, "classification": "commercial" }
],
"ai_overview": { "summary": ["..."], "markdown": "...", "text_blocks": [ { "type": "paragraph", "text": "..." }, { "type": "list", "items": ["..."] } ], "citations": [ { "rank": 1, "title": "...", "url": "https://...", "domain": "example.com", "snippet": "..." } ] },
"aio_status": "present",
"serp_features": { "featured_snippet": { ... }, "people_also_ask": [ ... ], "related_searches": [ ... ] }
}serpapi (compatible)
output=serpapi
{
"search_metadata": { "id": "9f3c1e2a-...", "status": "done" },
"search_parameters": { "q": "best running shoes", "location_requested": "Hanoi, Vietnam", "gl": "vn", "hl": "vi", "device": "desktop" },
"organic_results": [
{ "position": 1, "title": "...", "link": "https://...", "displayed_link": "...", "snippet": "...", "favicon": "...", "sitelinks": { "inline": [ ... ] } }
],
"ai_overview": { "text_blocks": [ { "type": "paragraph", "snippet": "..." }, { "type": "list", "list": [ { "snippet": "..." } ] } ], "markdown": "...", "references": [ { "title": "...", "link": "https://...", "source": "example.com", "snippet": "...", "index": 1 } ] },
"answer_box": { ... },
"related_questions": [ ... ],
"related_searches": [ ... ]
}Not byte-identical to SerpApi
The
serpapi format mirrors SerpApi’s field names where practical to ease migration, but does not claim byte-for-byte parity. Field coverage tracks what the parser extracts.Completeness & partials
completeness (native output) is the fraction of available results collected: collected / min(target, total_results). A keyword with fewer than 100 results in existence scores 1.0 once all of them are taken — not a false shortfall. A partial status with a high completeness ratio is a fully-covered short keyword; a low ratio indicates the crawl was blocked before finishing.
Error responses
| Field | Type | Description |
|---|---|---|
| 400 | error | Invalid output value (must be html, json, or serpapi). |
| 404 | error | No job with that id. |
| 422 | error | Job finished but its stored HTML is unavailable, so it cannot be parsed. |
Full catalogue on Errors.