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
FieldTypeDescription
job_idrequiredstringThe id returned by POST /v1/search (path parameter).
outputstring"html" (default), "json", or "serpapi". Controls the response shape.
numintegerMaximum 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.
startintegerResult 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:

FieldTypeDescription
queuednon-terminalAccepted and waiting for a worker.
runningnon-terminalThe crawl is in progress.
doneterminalCompleted; results are available. Check meta.complete: if a result page could not be fetched, the crawl is still done and meta.missing_pages lists the gap (meta.coverage shows e.g. "46/100").
failedterminalThe 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 / failed
    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.

FieldTypeDescription
htmldefaultReturns job metadata plus a presigned URL to the raw assembled SERP HTML. Re-parseable later; the source of truth for every other format.
jsonnativeOur own stable schema: search_metadata, search_parameters, organic_results, ai_overview, serp_features.
serpapicompatibleSerpApi-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,
    "complete": true, "coverage": "100/100", "missing_pages": [],
    "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,
    "complete": true, "coverage": "100/100", "missing_pages": []
  },
  "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 & missing pages

Every finished crawl reports how much it collected. coverage is collected/expected, where expected is what a complete crawl of that keyword returns: a keyword Google only has 17 results for reads 17/17, not 17/100. complete is true when nothing is missing.

When a result page could not be fetched, the job is still done and returns everything it did collect, but complete is false, coverage reads e.g. 46/100, and missing_pages lists the 1-based pages that are missing, e.g. [6]. Results after a missing page keep room for it: their position skips 10 per missing page (page 7's first result is 61, not 51), so a gap in the numbering is where the missing page sits. Those positions are estimates that can be off by the few results a page gives to packs. A short keyword is never incomplete: having fewer results than the target is not a gap.

completeness (native output) is the same picture as a fraction: collected / min(target, total_results).

Error responses

FieldTypeDescription
400errorInvalid output value (must be html, json, or serpapi).
404errorNo job with that id.
422errorJob finished but its stored HTML is unavailable, so it cannot be parsed.

Full catalogue on Errors.