Skip to content
Start free
Back to Blog

SERP Scraping API Guide: Google, Bing, DuckDuckGo, Yandex

Suciu DanLast updated on 20 min read
SERP Scraping API Guide: Google, Bing, DuckDuckGo, Yandex
TL;DR: To use the SERP Scraping API, send a GET request to https://serpapi.webscrapingapi.com/v2 with api_key, engine, and your query (q for most engines, text for Yandex). Google and Bing return parsed JSON, while DuckDuckGo and Yandex return the results page's HTML for your code to extract, so plan parsing and tests around that split. This guide also covers localization, async jobs, errors, and pricing.

You need search results for a set of keywords, in specific countries and languages, on specific devices, as data your code can store and compare week over week. Running your own search scraper means fighting blocks and rewriting parsers every time a results page changes, so this guide uses WebScrapingAPI's SERP API instead. By the end, you'll send a first request with your key in an environment variable, choose the right engine, fix the market you're measuring, parse Google and Bing JSON defensively, extract DuckDuckGo and Yandex HTML yourself, run Google searches asynchronously, retry by the documented error contract, and size a plan.

Quick start

  • Endpoint: https://serpapi.webscrapingapi.com/v2 (GET)
  • Required: api_key, engine, and the query (q for most engines, text for Yandex)
  • Output: Google and Bing return parsed JSON. DuckDuckGo and Yandex return the result page's HTML inside a JSON envelope
  • Free trial: 100 Google searches

What is the SERP Scraping API, and what does it return?

WebScrapingAPI's SERP API is a server-side endpoint that takes a search query, an engine, and market context, then returns the result page as data. Google and Bing come back as parsed JSON collections. DuckDuckGo and Yandex come back as a JSON envelope whose search_results field holds the SERP HTML for your code to extract.

That split is the most important thing to understand before you write any code. With Google and Bing, you read named collections such as organic[] and related[]. With DuckDuckGo and Yandex, you receive the raw page and decide what to pull out of it. Your storage model, your tests, and your maintenance work all follow from which side of that line an engine sits on.

The API covers four engine families: Google, Bing, DuckDuckGo, and Yandex. Google also has seven specialized engines next to standard search: async search, Maps, Maps Reviews, Hotels, Flights, Trends, and Reverse Image. Teams use them for rank and SERP-feature monitoring, brand and publisher presence, returned paid placements, and market research across topics and publishers.

  • WebScrapingAPI operates: request validation and engine execution, the search access infrastructure, parsed result collections where they're documented, delivery of the JSON envelope for HTML engines, and maintenance of supported endpoint behavior.
  • Your team owns: query groups, markets, languages, and devices, plus scheduling, API-level retries, extraction from HTML envelopes, storage, rank calculations, alerts, and data-use and retention policy.

One more boundary matters for analytics. The API doesn't define a universal rank. Every result describes one query, in one market, on one device, at one moment, so keep that context attached to every row you store. Comparing it with the other request-time APIs? See how the SERP API fits next to the other Web Access APIs.

How do you send your first SERP API request?

Create an account, subscribe to the SERP API in the dashboard, and copy your API key into an environment variable. Then send a GET request to https://serpapi.webscrapingapi.com/v2 with api_key, engine=google, and q set to your keyword. Let your HTTP library encode the parameters, and keep the key on the server.

  1. Register a WebScrapingAPI account.
  2. Subscribe to the SERP API in the dashboard. The trial includes 100 Google searches.
  3. Store the key in your shell or secrets manager: export WSA_API_KEY=...
  4. Send the request with one of the snippets below.

The request has the same shape for every engine. Only engine, the query field, and the context parameters change, so once this call works, every other engine is a matter of parameters.

curl. The --data-urlencode flags encode each value, and --max-time 120 sets an explicit client timeout:

# Google, parsed JSON, US English, mobile
EP="https://serpapi.webscrapingapi.com/v2"
curl --get --fail-with-body --max-time 120 "$EP" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=google" \
  --data-urlencode "q=running shoes" \
  --data-urlencode "gl=us" --data-urlencode "hl=en" --data-urlencode "device=mobile"

Python. This small client reads the key from the environment, passes every value through the params argument so Requests encodes it for you, retries by the error contract, and records the usage headers. The Bing and error-handling sections below reuse it.

"""Minimal WebScrapingAPI SERP API client: env-var key, jittered retries, usage headers.

Usage:
  export WSA_API_KEY=...                       # never hardcode the key
  python wsa_serp.py google "running shoes" gl=us hl=en device=mobile
  python wsa_serp.py bing "coffee roasters" mkt=en-US cc=us lang=en
  python wsa_serp.py yandex "pizza" lr=213 p=0
  python wsa_serp.py google "running shoes" gl=us --dry-run   # print the request, no call
"""
import os
import random
import sys
import time

import requests

ENDPOINT = "https://serpapi.webscrapingapi.com/v2"
# Most engines take their main input in q. These don't (see the engine table).
QUERY_FIELD = {"yandex": "text", "google_maps_reviews": "fid", "google_reverse_image": "url"}
RETRYABLE = {429, 500, 502, 503}   # back off with jitter, then retry
FINAL = {400, 401}                 # fix the key, plan, or parameters; never retry


def build_params(engine, query, **extra):
    params = {"engine": engine, QUERY_FIELD.get(engine, "q"): query}
    params.update({k: str(v) for k, v in extra.items()})
    return params


def search(engine, query, api_key, max_attempts=4, **extra):
    params = {"api_key": api_key, **build_params(engine, query, **extra)}
    for attempt in range(1, max_attempts + 1):
        resp = requests.get(ENDPOINT, params=params, timeout=120)
        usage = {k: v for k, v in resp.headers.items() if k.lower().startswith("searchdata-")}
        if resp.ok:
            return resp.json(), usage
        if resp.status_code in FINAL:
            # 401: missing, invalid, or unentitled key. 400: "Parameter '<name>' not allowed." etc.
            raise SystemExit(f"{resp.status_code}: {resp.text[:300]}")
        retry_422 = resp.status_code == 422 and attempt == 1   # target-side failure: one retry only
        if resp.status_code in RETRYABLE or retry_422:
            time.sleep(random.uniform(0, min(2 ** attempt, 30)))   # full jitter
            continue
        raise SystemExit(f"Unexpected {resp.status_code}: {resp.text[:300]}")
    raise SystemExit(f"Gave up after {max_attempts} attempts")


def organic(data):
    """Product pages document organic[]; some docs pages show organic_results[]. Accept both."""
    return data.get("organic") or data.get("organic_results") or []


def main():
    args = [a for a in sys.argv[1:] if a != "--dry-run"]
    if len(args) < 2:
        sys.exit(__doc__)
    engine, query = args[0], args[1]
    extra = dict(a.split("=", 1) for a in args[2:])

    if "--dry-run" in sys.argv:
        prepared = requests.Request(
            "GET", ENDPOINT, params={"api_key": "REDACTED", **build_params(engine, query, **extra)}
        ).prepare()
        print(prepared.url)
        return

    api_key = os.environ.get("WSA_API_KEY")
    if not api_key:
        sys.exit("Set WSA_API_KEY first (dashboard -> SERP API).")

    data, usage = search(engine, query, api_key, **extra)
    print("usage headers:", usage)
    if "search_results" in data:          # DuckDuckGo and Yandex: SERP HTML inside a JSON envelope
        print("search_parameters:", data.get("search_parameters"))
        print("html chars:", len(data["search_results"]))
    else:                                  # Google and Bing: parsed JSON
        for row in organic(data)[:10]:
            print(row.get("rank") or row.get("position"), row.get("title"), row.get("link"))


if __name__ == "__main__":
    main()

Run python wsa_serp.py google "running shoes" gl=us hl=en device=mobile --dry-run first. It prints the exact request URL with the key replaced by REDACTED, so you can check the parameters before you spend a search.

Node.js (18 or later). URLSearchParams handles the encoding, and AbortSignal.timeout sets the same 120-second limit:

// Build and (optionally) send a SERP API request in Node 18+.
// URLSearchParams handles encoding; the key comes from the environment.
const ENDPOINT = "https://serpapi.webscrapingapi.com/v2";
const QUERY_FIELD = { yandex: "text", google_maps_reviews: "fid", google_reverse_image: "url" };

export function buildSerpUrl(apiKey, engine, query, extra = {}) {
  const url = new URL(ENDPOINT);
  url.searchParams.set("api_key", apiKey);
  url.searchParams.set("engine", engine);
  url.searchParams.set(QUERY_FIELD[engine] ?? "q", query);
  for (const [k, v] of Object.entries(extra)) url.searchParams.set(k, String(v));
  return url;
}

export async function search(engine, query, extra = {}) {
  const apiKey = process.env.WSA_API_KEY;
  if (!apiKey) throw new Error("WSA_API_KEY is required");
  const res = await fetch(buildSerpUrl(apiKey, engine, query, extra), {
    signal: AbortSignal.timeout(120_000),
  });
  if (!res.ok) throw new Error(`SERP API ${res.status}: ${(await res.text()).slice(0, 300)}`);
  const data = await res.json();
  return { data, organic: data.organic ?? data.organic_results ?? [] };
}

// Dry run: show the encoded URL and prove the query survives the round trip.
if (process.argv[1]?.endsWith("build_serp_request.mjs")) {
  const q = 'site:example.com "running shoes" & socks?';
  const u = buildSerpUrl("REDACTED", "google", q, { gl: "us", hl: "en", device: "mobile" });
  console.log(u.toString());
  console.log("round trip ok:", new URL(u).searchParams.get("q") === q);
  const y = buildSerpUrl("REDACTED", "yandex", "pizza", { lr: 213, p: 0 });
  console.log(y.toString());
}

The dry run shows why encoding matters. The query site:example.com "running shoes" & socks? becomes q=site%3Aexample.com+%22running+shoes%22+%26+socks%3F and decodes back unchanged (round trip ok: true). If you glue that query into a URL by hand, the bare & ends your q value and starts a new parameter named socks?. The /v2 endpoint validates parameters per engine and rejects names it doesn't recognize, so a hand-built URL fails in confusing ways. Let the library apply percent-encoding every time.

Treat the key like a password. Keep it in server-side environment variables or a secrets manager, and never put it in front-end code, public repositories, logs, or screenshots. Anyone with the key can spend your searches.

Docs: Access the API

Which search engines and Google engines can you query?

The current API reference lists eleven engines: google, google_async, google_maps, google_maps_reviews, google_flights, google_hotels, google_trends, google_reverse_image, bing, duckduckgo, and yandex. Each has its own required input and allowed parameters. Treat only these as supported, even if older documentation pages describe other Google engines.

Engine

Required input

You get back

Pricing

google

q

Parsed JSON (general, input, navigation[], organic[], optional modules)

Google plans

google_async

q

response_id, then the finished SERP payload

Google plans

google_maps

type=search + q, or type=place + data

Parsed place results

Google plans

google_maps_reviews

fid

Parsed reviews

Google plans

google_hotels

type=search + q, or type=place + pid; check-in and check-out dates

overview, organic[]

Google plans

google_flights

q (encoded Google Flights token)

search_parameters, search_results[]

Google plans

google_trends

q

widgets[] (TIMESERIES, GEO_MAP)

Google plans

google_reverse_image

url

general, images[], exact_matches[]

Google plans

bing

q

Parsed JSON (general, organic[], related[])

Quote

duckduckgo

q

JSON envelope: search_parameters + search_results (HTML)

Quote

yandex

text

JSON envelope: search_parameters + search_results (HTML)

Quote

Three engines don't take their main input in q. Yandex takes the query in text, Google Maps Reviews takes a place's fid, and Google Reverse Image takes an image url. The Python and Node clients above already map those fields for you. Google Maps and Google Hotels also need type=search or type=place, which changes the other required field.

"Google plans" means the published monthly Google plans covered in the pricing section. Bing, DuckDuckGo, and Yandex are priced by quote.

You may find documentation pages for other Google engines, such as autocomplete, events, finance, jobs, local services, Maps photos, Play, product, and Scholar. They aren't in the supported list, so don't build a pipeline on them.

Docs: SearchData and SERP APIs reference

How do you localize Google results by country, language, and device?

Set gl to the two-letter country, hl to the interface language, and device to desktop, mobile, or tablet. Add domain for a specific Google host and uule for an encoded location when city-level context matters. Store these values with every result, because the same query can rank differently in each market.

Google itself says that results depend on where you search from, so a rank without its market is half a fact. This Canadian, English-language, desktop search for "history" on google.ca runs through the Python client from the first-request section:

python wsa_serp.py google history gl=ca hl=en domain=google.ca device=desktop

If you need a specific city, such as London, Ontario, add its uule value as one more uule=... argument. Use only the parameters your engine documents. Older examples that sort by date or filter by time period don't apply to /v2 Google Search, and the endpoint rejects parameters it doesn't allow.

Results type and pagination also change what you get back:

  • Surfaces: tbm switches the results type: isch for images, vid for videos, nws for news, and shop for shopping. For Google Jobs results, use ibp=htl;jobs.
  • Pagination: start sets how many results to skip, and num sets how many results to return per page.

The most useful habit here is market design. Before you collect anything, choose a small, fixed grid of query × country × language × device, and don't change it between runs. If one week's run uses gl=us&device=mobile and the next uses gl=us&device=desktop, a "rank drop" may only be a device change. Each response is one observation of one market, not a universal truth, so comparisons only mean something inside the same grid cell.

The full list of request fields and response blocks is on the Google Search API controls page.

Engine

Location / market

Language

Device

Paging / time

Filters and surfaces

Google

gl (country), domain (Google host), uule (encoded location)

hl

device = desktop, mobile, tablet

start (skip), num (per page)

tbm = isch, vid, nws, shop; ibp=htl;jobs

Bing

mkt (e.g., en-US), cc, location, lat, lon

lang

—

first (offset), count

safesearch = off, moderate, strict

DuckDuckGo

kl (e.g., en_US)

kad (interface)

device

df = d, w, m, y

kp = 1, -1, -2

Yandex

lr (region ID, e.g., 213), yandex_domain

lang

device

p (zero-based), within = 77 (24 h), 1 (2 weeks)

—

Docs: Google Search API parameters

How do you read and store parsed Google and Bing results?

Read the organic array for ranked results, each with a title, link, display link, description, and rank. Treat every other module, such as related searches, navigation, ads, or local results, as optional. Store each row with the query, market, device, and capture time so later rank comparisons stay meaningful.

A Google response carries a few top-level blocks. general describes the request (engine, language, location, mobile flag, and search type), input holds the original search URL, navigation[] lists other Google surfaces for the query, and organic[] holds the ranked results. A Bing response carries general, organic[], and related[].

Everything else is conditional. Ads, local results, shopping, media, and related searches appear only when the result page shows them, and which SERP features Google shows changes with the query, location, and device. So keep two cases apart in your code: a request that failed (an HTTP error) and a request that succeeded with a module absent. The second is a normal observation, not a bug.

Write the parser defensively. Some documentation pages show the ranked list as organic_results[] with position instead of organic[] with rank, so read either. This script flattens a response into rank rows. The input file is a labeled sample, not live output:

"""Flatten a Google or Bing SERP API response into rank rows.

Organic results may arrive as organic[] (product pages) or organic_results[] (some docs pages),
and modules such as related[] or navigation[] are optional, so read them defensively.
"""
import json
import sys
from datetime import datetime, timezone


def rank_rows(data, query, market):
    rows = []
    captured = datetime.now(timezone.utc).isoformat(timespec="seconds")
    for item in data.get("organic") or data.get("organic_results") or []:
        rows.append({
            "captured_at": captured,
            "engine": data.get("general", {}).get("search_engine"),
            "query": query,
            "market": market,
            "rank": item.get("rank") or item.get("position"),
            "title": item.get("title"),
            "link": item.get("link"),
        })
    return rows


if __name__ == "__main__":
    data = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "sample_google.json"))
    for row in rank_rows(data, "running shoes", "us/en/mobile"):
        print(row)
    print("optional modules present:", [k for k in ("navigation", "related", "related_questions") if k in data])

On the sample file, it prints one row per organic result and lists which optional modules were present:

{'captured_at': '2026-10-29T12:03:56+00:00', 'engine': 'google', 'query': 'running shoes', 'market': 'us/en/mobile', 'rank': 1, 'title': 'Example result one', 'link': 'https://example.com/a'}
{'captured_at': '2026-10-29T12:03:56+00:00', 'engine': 'google', 'query': 'running shoes', 'market': 'us/en/mobile', 'rank': 2, 'title': 'Example result two', 'link': 'https://example.org/b'}
optional modules present: ['navigation']

A rank row should carry at least captured_at, engine, query, market, device, rank, title, and link. With those fields, you can compare runs, chart movement, and alert on drops. That's the core of monitoring rankings and SERP features over time. If your stored links include Google redirect wrappers, read about Google /goto redirect links in result URLs before you deduplicate by URL.

How do you search Bing with the SERP API?

Set engine=bing and pass the query in q. Control the market with mkt, such as en-US, plus cc for country and lang for language. Add location, or lat and lon, for a precise origin, safesearch for filtering, and first with count to page through results. Bing responses arrive as parsed JSON.

The curl version for a US market, first page of 10 results:

curl --get --fail-with-body --max-time 120 "https://serpapi.webscrapingapi.com/v2" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=bing" \
  --data-urlencode "q=coffee roasters" \
  --data-urlencode "mkt=en-US" --data-urlencode "cc=us" --data-urlencode "lang=en" \
  --data-urlencode "first=0" --data-urlencode "count=10"

With the Python client from the first-request section, the same call is one line:

python wsa_serp.py bing "coffee roasters" mkt=en-US cc=us lang=en first=0 count=10

Bing returns general, organic[], and related[]. Ads and images are optional modules, so the same defensive rules apply: read organic[], treat everything else as present-or-absent, and store each row with its market. safesearch takes off, moderate, or strict.

Keep mkt, cc, and lang consistent across runs, just as you would with Google's gl and hl. Bing is priced by quote, and the 100-search trial covers Google. Request fields and response blocks are on the Bing Search API page.

Docs: Bing basic API requests

How do you handle DuckDuckGo and Yandex HTML results?

Both engines return JSON with search_parameters, describing the request, and search_results, a string holding the source SERP HTML. WebScrapingAPI doesn't parse that HTML, so your application extracts the fields it needs. Pin your selectors to a real capture, version them, store the raw envelope, and alert when a selector stops matching.


Google, Bing

DuckDuckGo, Yandex

Response

JSON metadata + parsed collections

JSON metadata + source SERP HTML in search_results

Parsing maintained by

WebScrapingAPI, where documented

Your application

Your code does

Read collections defensively; store rows

Pick and version selectors; extract; alert on drift

Store

Rows + request context

Raw envelope + extracted rows + selector version

DuckDuckGo request fields: q for the query, kl for region and language (such as en_US), kad for the interface language, kp for safe search (1 on, -1 moderate, -2 off), df for time (d, w, m, y), and device.

Yandex request fields: text for the query, lr for the region ID (for example, 213), lang, device, yandex_domain, p for the page (zero-based, so p=0 is the first page), and within for time (77 for the past 24 hours, 1 for the past two weeks).

# DuckDuckGo, SERP HTML in a JSON envelope
curl --get --fail-with-body --max-time 120 "https://serpapi.webscrapingapi.com/v2" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=duckduckgo" \
  --data-urlencode "q=privacy research" \
  --data-urlencode "kl=en_US" --data-urlencode "df=w"

# Yandex, SERP HTML in a JSON envelope (query parameter: text)
curl --get --fail-with-body --max-time 120 "https://serpapi.webscrapingapi.com/v2" \
  --data-urlencode "api_key=$WSA_API_KEY" \
  --data-urlencode "engine=yandex" \
  --data-urlencode "text=pizza" \
  --data-urlencode "lr=213" --data-urlencode "p=0"

The extraction pattern is the same for both engines: validate the envelope, parse the HTML, select result links, drop the engine's own navigation links, tag each row with a selector version, fail loudly when nothing matches, and keep the raw HTML so you can reprocess it later. This script uses Beautiful Soup with CSS selectors:

"""Pull result links out of a DuckDuckGo or Yandex SERP API envelope.

These engines return {"search_parameters": {...}, "search_results": "<html>...</html>"}.
Your application owns extraction: pin the selector after inspecting a real capture,
version it, and alert when it stops matching. The selectors below fit the synthetic
fixtures only.
"""
import json
import sys
from urllib.parse import urlparse

from bs4 import BeautifulSoup

SELECTORS = {"duckduckgo": "div.result a.result-link", "yandex": "li.serp-item a.link"}
ENGINE_HOSTS = ("duckduckgo.com", "yandex.com", "yandex.ru")


def extract(envelope, selector_version="fixture-v1"):
    params = envelope.get("search_parameters", {})
    engine = params.get("search_engine")
    html = envelope.get("search_results")
    if not isinstance(html, str) or "<html" not in html.lower():
        raise ValueError("search_results is missing or is not HTML; store the raw envelope and alert")
    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for pos, a in enumerate(soup.select(SELECTORS[engine]), start=1):
        host = urlparse(a.get("href", "")).hostname or ""
        if host.endswith(ENGINE_HOSTS):
            continue  # skip the engine's own navigation links
        rows.append({"engine": engine, "query": params.get("query"), "position": pos,
                     "title": a.get_text(strip=True), "link": a["href"],
                     "selector_version": selector_version})
    if not rows:
        raise ValueError(f"selector matched nothing for {engine}; markup may have changed")
    return rows


if __name__ == "__main__":
    for path in sys.argv[1:] or ["sample_ddg_envelope.json", "sample_yandex_envelope.json"]:
        for row in extract(json.load(open(path))):
            print(row)

The selectors in SELECTORS fit the synthetic sample files only. Real DuckDuckGo and Yandex markup is different, and it changes. Before you run this in production, save a real envelope, inspect its HTML, pin your own selectors, and bump selector_version whenever you change them. When the markup shifts, the ValueError fires instead of your pipeline quietly storing zero results, and because you kept the raw HTML, you can re-extract the affected runs once the selectors are fixed.

Engine-specific fields are on the DuckDuckGo Search API and Yandex Search API pages.

Docs: DuckDuckGo API parameters · Yandex API parameters

Use the specialized Google engines when the answer lives outside regular web results. Google Maps finds places, Maps Reviews reads a place's reviews by fid, Trends returns interest-over-time widgets, Hotels and Flights return travel results, and Reverse Image matches a picture by URL. Each engine has its own required input and parameters.

They all use the same endpoint, key, and encoding rules as standard search. Only the engine value and the fields change:

  • google_maps: send type=search with q, or type=place with data. ll=@lat,lon,zoom sets the map view, and place_type narrows to hotels or vacation rentals. No pagination is documented.
  • google_maps_reviews: send the place's fid, which you take from the knowledge.fid field of a Google Search response. Sort with sort (qualityScore, newestFirst, ratingHigh, ratingLow) and page with start and num.
  • google_trends: send q with geo, date (today 12-m, now 1-H, or a YYYY-MM-DD YYYY-MM-DD range), cat, and gprop (images, news, froogle, youtube). The response holds widgets[], such as TIMESERIES and GEO_MAP.
  • google_hotels: send type=search with q, or type=place with pid, plus check_in_date and check_out_date. Occupancy, currency, and free-cancellation filters are optional.
  • google_flights: q is an encoded Google Flights query token, not plain text. Add curr, hl, and gl for currency and market.
  • google_reverse_image: send the image's url, and use lens (products, visual_matches, exact_matches, homework) to choose the match type.

If you're deciding between reverse image search and tbm=isch image results, see Google image search APIs compared.

Docs: Google search engines

How do you run Google searches asynchronously?

Send the search with engine=google_async. The API queues it and returns a response_id right away. Store that ID with your job, then poll the same /v2 endpoint with snapshot_id set to the ID and type=serp every 10 to 30 seconds until the finished SERP payload arrives. Cap the total wait time.

Async fits large batches and workers that can't hold a 120-second request open. It accepts the regular Google fields plus device and response=json. It's Google-only: Bing, DuckDuckGo, and Yandex don't have an async engine, so don't design a shared async queue around them.

"""Submit a google_async job, then poll for the finished SERP payload.

Usage:
  export WSA_API_KEY=...
  python google_async.py "events in new york city"
  python google_async.py "events in new york city" --dry-run
"""
import os
import random
import sys
import time

import requests

ENDPOINT = "https://serpapi.webscrapingapi.com/v2"


def submit(api_key, query, **extra):
    r = requests.get(ENDPOINT, params={"api_key": api_key, "engine": "google_async", "q": query, **extra}, timeout=60)
    r.raise_for_status()
    return r.json()["response_id"]          # store this id with your job record


def poll(api_key, response_id, max_wait=600):
    deadline = time.monotonic() + max_wait
    while time.monotonic() < deadline:
        r = requests.get(ENDPOINT, params={"api_key": api_key, "snapshot_id": response_id, "type": "serp"}, timeout=60)
        if r.ok:
            body = r.json()
            if body.get("status") in (None, "finished"):
                return body                  # persist it; don't re-poll old ids later
        elif r.status_code not in (429, 500, 502, 503):
            raise SystemExit(f"{r.status_code}: {r.text[:300]}")
        time.sleep(random.uniform(10, 30))   # docs: poll every 10 to 30 seconds
    raise SystemExit(f"No result for {response_id} after {max_wait} s")


if __name__ == "__main__":
    query = sys.argv[1] if len(sys.argv) > 1 else "events in new york city"
    if "--dry-run" in sys.argv:
        for p in ({"api_key": "REDACTED", "engine": "google_async", "q": query},
                  {"api_key": "REDACTED", "snapshot_id": "<response_id>", "type": "serp"}):
            print(requests.Request("GET", ENDPOINT, params=p).prepare().url)
        sys.exit(0)
    key = os.environ.get("WSA_API_KEY") or sys.exit("Set WSA_API_KEY first.")
    rid = submit(key, query)
    print("response_id:", rid)
    print(str(poll(key, rid))[:500])

Persist each finished result as soon as it arrives, and don't come back later to re-poll old IDs. The loop has a hard deadline and treats any unexpected status as a stop, so a stuck job can't hold a worker forever.

Docs: Async APIs

How should your code handle SERP API errors and retries?

Don't retry 401 or validation errors. Fix the key, the plan, or the parameters. Retry 429, 500, 502, and 503 a few times with exponential backoff and jitter, and slow down on 429. A 422 means the search engine returned an error, timeout, or challenge, so retry it once only if the failure looks transient.

Status

Meaning

Action

401

No key, invalid key, or key not entitled to the SERP API

Fix the key or plan. Don't retry.

Validation error

Unknown, missing, or out-of-range parameter for the chosen engine

Fix the request. Don't retry.

422 (error_type: "website")

The search engine returned an invalid body, a challenge, a timeout, or an error status

Retry once with backoff if it looks transient; then log and skip

429

Too many concurrent requests

Slow down, back off with jitter, retry

500, 503

Unexpected handler error or transient auth backend failure

Retry a few times with backoff

502

Async submission got an invalid response

Retry the submission with backoff

Authentication errors are easy to recognize. A request with no key returns 401 with "Access denied. No key api_key provided.", and a wrong or unentitled key returns 401 with Authentication failure: Invalid API key…. Retrying either one only repeats the failure.

Validation messages name the problem directly: Parameter '<name>' not allowed., Parameter '<name>' is required., Parameter '<name>' must be in range …, and Parameter '<a>' cannot be used with parameter '<b>'. That's why parameters copied from old examples fail on /v2: each engine accepts only its own documented fields.

A 429 Too Many Requests means you're sending too many requests at once. Lower your concurrency and back off before retrying. For 500, 502, and 503, a few retries with exponential backoff and full jitter are enough, and the random delays keep a fleet of workers from retrying in lockstep. The search() function in the Python client implements this whole policy: it stops on 401, retries the retryable codes with jittered sleeps, retries a 422 once, and gives up after four attempts.

Log two response headers on every call for cost tracking: Searchdata-Call-Credit-Usage (credits used by this call) and Searchdata-Current-Credit-Usage (current account usage). The client returns them as usage. Log the status, the headers, and the request context, but never the key.

Keep HTTP failures and empty modules apart in your metrics. A 200 response with no related[] block is a successful search, and it shouldn't trigger a retry.

Docs: Errors and headers

How much does the SERP API cost?

Google plans are sized by monthly searches: Starter is $28 for 10,000, Grow $130 for 50,000, Business $550 for 250,000, and Pro $1,600 for 1,000,000. The trial includes 100 Google searches for $0. Bing, DuckDuckGo, and Yandex are priced by quote for your workload, as of October 2026.

Plan

Price per month

Google searches

Effective per 1,000

Trial

$0

100

—

Starter

$28

10,000

$2.80

Grow

$130

50,000

$2.60

Business

$550

250,000

$2.20

Pro

$1,600

1,000,000

$1.60

Bing, DuckDuckGo, Yandex

Quote

Workload-based

—

Prices are in USD, as of October 2026. Every plan, including the trial, includes Google product data, global locations, SERP parsing, bulk processing, all devices, JSON, CSV, and HTML output, and image search. Plans differ by monthly search allowance.

To size a plan, multiply the parts of your market grid:

Monthly searches = keywords × markets × devices × result types × refresh runs per month

Then add headroom for testing and for keywords you'll add later. For example, 500 keywords × 2 countries × 2 devices × 4 weekly runs = 8,000 searches a month, which fits Starter with room to spare. Move the same project to daily runs and it becomes about 60,000 searches, which moves you to Business. Refresh frequency multiplies everything else, so set it by how often the decisions that depend on the data actually get made.

Use the trial to validate your representative queries, markets, devices, and output fields before you commit to a volume. If you're also weighing other vendors, see how SERP API pricing compares across providers.

What doesn't the SERP API do, and when is another tool better?

The SERP API returns search result pages, not the pages behind each link, and it doesn't define a universal rank. DuckDuckGo and Yandex extraction is your code's job, async is Google-only, and Google AI Mode answers are a separate product. Use the Scraper API, the Data API, or a managed feed when those limits matter.

  • It stops at the results page. To fetch the pages behind the links, use the Scraper API. See how to use the Scraper API.
  • AI Mode is separate. For Google AI Mode answers, citations, and brand mentions, use Google AI Mode data from the Data API.
  • It's request-time, not a feed. If you need recurring, governed records delivered on a schedule instead of making calls yourself, look at search-result data feeds.
  • Only listed engines are supported. Build on the eleven engines in the reference, not on older docs pages.
  • HTML engines need upkeep. DuckDuckGo and Yandex selectors are yours to maintain when their markup changes.
  • Async is Google-only. Plan synchronous workers for Bing, DuckDuckGo, and Yandex.
  • One result is one observation. A rank describes one query, market, device, and moment. Whether an absence matters commercially is your call, not the API's.
  • Use is governed. Collect eligible public results in line with WebScrapingAPI's Service Agreement and the requirements that apply to you.

Frequently asked questions

What is a SERP API?

A SERP API is a web service that runs a search for you and returns the search engine results page as data. You send a query, an engine, and context such as country, language, and device. WebScrapingAPI's SERP API returns parsed JSON for Google and Bing and HTML in a JSON envelope for DuckDuckGo and Yandex.

How do you use a SERP API?

Store your API key in an environment variable, then send a GET request to https://serpapi.webscrapingapi.com/v2 with api_key, engine, and your query. Add market controls such as gl and hl for Google, read the organic results from the JSON response, and save each row with its query, market, device, and capture time.

How do I get a SERP API key?

Register a WebScrapingAPI account, then subscribe to the SERP API from the dashboard to get your key. The trial includes 100 Google searches. Keep the key in an environment variable or secrets manager on your server, and never put it in browser code, public repositories, logs, or screenshots.

Is there a free trial for the SERP API?

Yes. The SERP API trial costs $0 and includes 100 Google searches, enough to test your keywords, markets, devices, and output before choosing a plan. Paid Google plans start at $28 a month for 10,000 searches. Bing, DuckDuckGo, and Yandex are priced by quote for your specific workload.

What is SERP scraping?

SERP scraping is collecting the results a search engine shows for a query, such as titles, links, positions, and features like related searches. Doing it yourself means handling blocks and layout changes. A SERP API moves that work to the provider, so your code sends a request and receives results as data.

Can I use the SERP API with Python?

Yes. Install Requests, read your key from an environment variable, and pass api_key, engine, and q in the params argument so Requests encodes them. Set a timeout of about 120 seconds, retry 429 and 5xx responses with backoff, and read the organic list from the JSON response for Google or Bing.

Which search engines does the SERP API support?

It supports Google, Bing, DuckDuckGo, and Yandex. Google also has specialized engines for async search, Maps, Maps Reviews, Hotels, Flights, Trends, and Reverse Image. Google and Bing return parsed JSON. DuckDuckGo and Yandex return the result page's HTML inside a JSON envelope, which your application parses.

Does the SERP API solve CAPTCHAs?

No SERP API page promises that. WebScrapingAPI operates the search access infrastructure behind each request, and when a search engine returns a challenge page, an error status, or a timeout, the API responds with a 422 website error instead of passing bad data along. Retry once if the failure looks temporary, then log it.

Why do DuckDuckGo and Yandex return HTML instead of parsed results?

For these two engines, the SERP API returns the source result page as HTML inside a JSON envelope rather than parsed collections. Your application chooses what to extract. Inspect a real response, pin CSS selectors to it, version them, keep the raw HTML, and alert when a selector stops matching after a layout change.

How do I track keyword rankings with a SERP API?

Pick a fixed set of keywords, countries, languages, and devices, then query them on a schedule. Save each organic result's rank, title, and link with the capture time and market. Compare runs only within the same market and device, and treat missing SERP features as an observation, not an error.

Start with 100 free Google searches

The SERP API returns Google and Bing results as parsed JSON and DuckDuckGo and Yandex result pages as HTML in a JSON envelope, with market, language, and device controls on every request. When you know your keywords, markets, and refresh schedule, compare SERP API plans and pick the allowance that fits.

About the Author

Suciu Dan, Co-founder @ WebScrapingAPI

Suciu Dan

Co-founder

Suciu Dan is the co-founder of WebScrapingAPI and writes practical, developer-focused guides on Python web scraping, Ruby web scraping, and proxy infrastructure.

Start Building

Ready to Scale Your Data Collection?

Join 2,000+ companies using WebScrapingAPI to extract web data at enterprise scale with zero infrastructure overhead.