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.
partialterminalCompleted with fewer than the full 100 results (blocks or an exhausted keyword). Results are still returned; check completeness.
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 / 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.

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,
    "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

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.