Guides

Guides & recipes

Practical patterns for common jobs: rank tracking, choosing async vs sync, caching, and migrating from SerpApi.

Rank tracking with stop_domain

To check where a single domain ranks for a keyword, pass stop_domain. The crawler stops paginating as soon as that domain appears (host-matched), which saves proxy egress and latency versus fetching all 100 results.

request
curl -X POST https://serp-api.hoangha.shop/v1/search \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "keyword": "best running shoes",
    "location": "Hanoi, Vietnam",
    "stop_domain": "example.com"
  }'

Then read the results and find the position of the row whose URL matches your domain. The AI Overview is captured on every crawl at no extra cost, since it reuses the same page-one load as the organic results.

Choosing async vs sync

Pick the flow that matches your latency and volume needs.

  • Async (default) — submit, then poll. Best for batches and high volume, and the only option once a crawl outlasts your HTTP client's timeout. Highest throughput.
  • Sync (async: false) — one blocking call, bounded by the server sync timeout. Best for a single interactive lookup where you want the answer in one request. May return a non-terminal status on slow crawls (the job still finishes in the background).
Even in sync mode you get a job_id, so a timed-out sync call can be finished by polling GET /v1/search/{job_id} later.

Running a batch efficiently

Submit all searches first (collect the job ids), then poll them. This keeps many crawls in flight at once instead of serializing on each result.

node.js
const keywords = ['running shoes', 'trail shoes', 'marathon shoes']

// 1. submit all
const jobs = await Promise.all(
  keywords.map((keyword) =>
    fetch('https://serp-api.hoangha.shop/v1/search', {
      method: 'POST',
      headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ keyword, location: 'Hanoi, Vietnam' }),
    })
      .then((r) => r.json())
      .then((j) => ({ keyword, jobId: j.job_id })),
  ),
)

// 2. poll each to completion (see the Retrieve results page for getResults)
const results = await Promise.all(
  jobs.map(({ keyword, jobId }) => getResults(jobId, KEY).then((data) => ({ keyword, data }))),
)

Respect the rate limit

Keep concurrent submissions under your key’s rate_limit_per_min. If you hit 429, back off and retry.

Getting the most from the cache

Repeat searches for the same keyword + location + device + mode + query knobs, within the cache window, return cached: true and cost no quota. To exploit this:

  • Normalize your inputs (same casing, same location string) so repeats actually hit.
  • Deduplicate a batch before submitting.
  • Change a keyed parameter only when you genuinely need a fresh crawl.

Migrating from SerpApi

Request output=serpapi and the response uses SerpApi’s field names (organic_results[].link, answer_box, related_questions, ai_overview.references, …). The main differences to account for:

  • This API is job-based: submit with POST /v1/search, then fetch with GET /v1/search/{job_id}?output=serpapi rather than a single synchronous call (use async: false if you want one blocking call).
  • Auth is the X-API-Key header, not an api_key query parameter.
  • Field coverage tracks what the parser extracts; it mirrors SerpApi where practical but is not byte-identical. Validate the fields your integration depends on.

See Retrieve results for full response examples in each format.