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.
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).
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.
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
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 withGET /v1/search/{job_id}?output=serpapirather than a single synchronous call (useasync: falseif you want one blocking call). - Auth is the
X-API-Keyheader, not anapi_keyquery 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.