Bulk SERP API

Bulk SERP API: batch Google searches, synchronously or as queued jobs

POST /serp/batch returns full Google results pages for up to 20 keywords in one synchronous call, and POST /async/task/batch queues up to 500 keywords per call to collect later or by webhook. Both cost 3 credits per keyword per results page.

GET/api/v1/search/web?q=postgres+connection+pooling&num=3
200 OK0.17s1 credit85,000 results
{  "searchTime": 0.17,  "totalResults": 85000,  "page": 1,  "organic": [    {      "position": 1,      "title": "31.10. Connection Pools and Data Sources",      "link": "www.postgresql.org/docs/…",      "domain": "postgresql.org",      "snippet": "For an environment without an application se…"    },    { "position": 2, "domain": "medium.com", … },    { "position": 3, "domain": "stackoverflow.blog", … }  ],  "credits": 1}

How do I get Google SERPs for hundreds of keywords at once?

Send up to 20 keywords to POST /serp/batch and get every results page back in one response, or queue up to 500 per call with POST /async/task/batch and collect them by polling or webhook. Each keyword costs 3 credits per results page, which is $0.90 per 1,000 keywords at the Scale pack rate.

A synchronous batch bills each distinct keyword once, including rows that fail. An async task that fails is refunded.

Synchronous
POST /serp/batch: up to 20 keywords, duplicates billed once
Queued
POST /async/task/batch: up to 500 tasks per call; poll GET /async/task/{taskId} or receive a webhook, signed when you set a secret
Price
3 credits per keyword per page: $0.60 to $2.40 per 1,000 keywords depending on the pack— our pricing
DataForSEO standard queue
$0.0006 per SERP of 10 results, about 5 minutes on average— dataforseo.com
SerpApi Production plan
$150 a month for 15,000 searches— serpapi.com/pricing

Third-party figures on this page last verified against the sources linked above. Prices change; if you find one of these stale, tell us and we will correct it.

Rank checks, SERP-feature audits and question research all start with a list of keywords, not one. Calling a SERP endpoint once per keyword works, but it leaves you writing the concurrency, retries and billing bookkeeping yourself. Searlo has two bulk paths so you do not have to.

POST /serp/batch is for lists you want back now. Up to 20 keywords go in one request, and the full results page for each comes back together: organic results, ads, videos, People Also Ask, related searches, knowledge graph and, if you ask for it, the AI Overview, with a summary of what succeeded. POST /async/task/batch is for everything bigger. It queues up to 500 SERP tasks per call for Searlo's workers to run, and you collect the results by polling or have each one pushed to your webhook as it finishes.

Sync batch or async tasks?

The two bulk paths side by side
PropertyPOST /serp/batchPOST /async/task/batch
Keywords per requestUp to 20Up to 500 tasks, one keyword each
What comes backEvery result, in the responseTask IDs immediately (202); each result when its task completes
Collecting resultsNothing to collectGET /async/task/{taskId}, or a webhook
Duplicate keywordsBilled once, ignoring case and surrounding spacesOne task each; an idempotencyKey stops a resubmit being queued twice
Failed keywordsReturned as rows with retryable and message; still billedRetried automatically when retryable; refunded if the task finally fails
Credits3 per distinct keyword per page, charged when the batch runs3 per keyword per page, held when queued and refunded on failure or cancel
Best forDashboards, small reports, interactive toolsNightly jobs, large keyword sets, anything over 20

Batch 20 keywords in one request

Body fields: keywords (1 to 20) and, optionally, gl, hl, location or uule, device (desktop, mobile or tablet), pages (1 to 10), num, tbs, cache and aioverview, which is off by default in a batch. One set of settings applies to every keyword in the request.

curl -X POST "https://api.searlo.tech/api/v1/serp/batch" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "keywords": ["best running shoes", "running shoes for flat feet", "trail running shoes"],
    "gl": "us",
    "hl": "en",
    "device": "mobile"
  }'
const res = await fetch("https://api.searlo.tech/api/v1/serp/batch", {
  method: "POST",
  headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    keywords: ["best running shoes", "running shoes for flat feet", "trail running shoes"],
    gl: "us",
    hl: "en",
  }),
});
const batch = await res.json();

for (const row of batch.results) {
  if (!row.success) {
    console.warn(`${row.keyword}: ${row.message} (retryable: ${row.retryable})`);
    continue;
  }
  const top = row.organic[0];
  console.log(row.keyword, "->", top?.domain, `(${row.peopleAlsoAsk.length} PAA questions)`);
}
console.log(`credits used: ${batch.credits.used}, failed rows: ${batch.meta.failed}`);
import requests

resp = requests.post(
    "https://api.searlo.tech/api/v1/serp/batch",
    json={
        "keywords": ["best running shoes", "running shoes for flat feet"],
        "gl": "us",
        "hl": "en",
        "aioverview": True,  # off by default in a batch; no extra credits
    },
    headers={"x-api-key": "YOUR_API_KEY"},
    timeout=600,  # the response arrives when every keyword is done
)
resp.raise_for_status()
batch = resp.json()

for row in batch["results"]:
    has_aio = bool(row.get("aioverview"))
    print(row["keyword"], row["success"], len(row.get("organic", [])), "AI Overview:", has_aio)

The batch response

Illustrative values, real keys, trimmed. Each successful row is a full SERP object plus its keyword; a failed row carries status, retryable and message instead. credits.used includes the failed row.

{
  "schema": "serp-batch.v1",
  "results": [
    {
      "keyword": "best running shoes",
      "success": true,
      "searchParameters": { "q": "best running shoes", "gl": "us", "hl": "en", "page": 1, "pages": 1, "num": 10, "device": "desktop", "type": "serp" },
      "organic": [
        { "position": 1, "title": "The best running shoes of 2026, tested", "url": "https://www.example-runner.com/best-running-shoes", "domain": "example-runner.com", "snippet": "…", "page": 1 }
      ],
      "ads": [],
      "videos": [],
      "peopleAlsoAsk": [{ "question": "Which running shoe brand is best?" }],
      "relatedSearches": [{ "query": "best running shoes for beginners" }],
      "knowledgeGraph": null,
      "aiModeAvailable": true,
      "fetchedAt": "2026-09-29T11:20:07.331Z",
      "cached": false
    },
    {
      "keyword": "trail running shoes",
      "success": false,
      "status": 429,
      "retryable": true,
      "message": "Google SERP is at capacity. Retry in 60s — immediate retries will be refused."
    }
  ],
  "credits": { "used": 9 },
  "meta": {
    "requested": 3,
    "distinct": 3,
    "succeeded": 2,
    "failed": 1,
    "pages": 1,
    "gl": "us",
    "hl": "en",
    "device": "desktop",
    "max_sync_batch": 20
  }
}

Queue 500 keywords with async tasks

One POST /async/task/batch call queues all 500. Each task holds its credits when queued and gets them back if it fails or is cancelled. The script polls until every task has finished and writes one JSON line per keyword; the idempotencyKey makes it safe to run twice.

import hashlib
import json
import time

import requests

API = "https://api.searlo.tech/api/v1"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
RUN = "2026-09-29"  # part of each idempotencyKey: re-running today re-uses the same tasks

with open("keywords.txt", encoding="utf-8") as f:
    keywords = list(dict.fromkeys(line.strip() for line in f if line.strip()))[:500]


def key_for(kw):
    return f"serp:us:en:{RUN}:" + hashlib.sha1(kw.encode()).hexdigest()  # keys max 128 chars


# 1. Queue everything in one request (up to 500 tasks per call).
tasks = [
    {
        "endpoint": "serp",
        "request": {"q": kw, "gl": "us", "hl": "en", "pages": 1, "aioverview": False},
        "idempotencyKey": key_for(kw),
    }
    for kw in keywords
]
queued = requests.post(f"{API}/async/task/batch", json={"tasks": tasks}, headers=HEADERS, timeout=120).json()
if "tasks" not in queued:
    raise SystemExit(f"batch rejected: {queued.get('message')}")

pending = {}  # taskId -> keyword, in queue order
for kw, item in zip(keywords, queued["tasks"]):
    if item["accepted"]:
        pending[item["taskId"]] = kw
    else:
        print("not queued:", kw, "-", item["error"])
for kw in keywords[len(queued["tasks"]):]:
    print("not queued (queueing stopped, usually out of credits):", kw)
print(f"queued {queued['accepted']} of {queued['total']}")

# 2. Poll until every task has finished.
with open("serps.jsonl", "a", encoding="utf-8") as out:
    while pending:
        time.sleep(20)
        for task_id, kw in list(pending.items()):
            task = requests.get(f"{API}/async/task/{task_id}", headers=HEADERS, timeout=30).json()["task"]
            if task["status"] == "COMPLETED":
                out.write(json.dumps({"keyword": kw, "serp": task["result"]}) + "\n")
                del pending[task_id]
            elif task["status"] in ("FAILED", "CANCELLED"):
                print("gave up:", kw, task.get("error"))  # its credits are refunded
                del pending[task_id]
            elif task["status"] == "QUEUED":
                break  # tasks run roughly in queue order: check the rest on the next pass
        print(f"{len(pending)} tasks still to finish")

Webhooks instead of polling

Add a webhook to each task, as "webhook": { "url": "https://your-app.example.com/searlo", "secret": "…" }, and Searlo POSTs the finished task to that URL when it completes or fails. The JSON body carries taskId, endpoint, status, result (or error), creditsUsed and completedAt. The headers carry X-Searlo-Task-Id, X-Searlo-Event (task.completed or task.failed) and X-Searlo-Timestamp.

With a secret set, X-Searlo-Signature is "v1=" followed by the hex HMAC-SHA256 of the timestamp header, a full stop and the raw body, computed with your secret. Recompute it on your side and compare before trusting the payload. A delivery that does not get a 2xx back is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. Webhook URLs must use HTTPS and resolve to a public address.

  • GET /async/status: how many of your tasks are queued or processing, and how many completed or failed in the last 24 hours.
  • DELETE /async/queue: cancel every task that is still queued and refund its credits.
  • priority (1 to 100, default 50): higher-priority tasks are picked up first.
  • idempotencyKey (up to 128 characters): resubmitting the same key returns the existing task instead of queuing and billing a second one.
  • expiresAt: each task says when its stored result expires, so collect results before then.

What bulk SERPs cost

3 credits per keyword per results page, converted at each pack's published rate. Packs are one-time purchases, not a subscription; credits on new accounts are valid for 90 days.
PackPricePer 1,000 keywords (one page)Keywords the pack covers
Micro$3.99$2.401,666
Starter$9.99$1.506,666
Builder$29.99$1.2025,000
Scale$74.99$0.9083,333
Pro$199.99$0.66300,000
Enterprise$799$0.601,333,333

500 keywords, one page each, is 1,500 credits: $0.45 at the Scale rate, and inside the 3,000 free credits of a new account. 10,000 keywords a week is about 43,300 keyword checks a month, or 130,000 credits: $39.00 at the Scale rate, with one Scale pack covering roughly eight weeks. Going deeper multiplies the cost: pages=3 (roughly the top 30) is 9 credits per keyword.

How that compares

Cost of 10,000 one-page Google SERPs at published prices, checked 2026-09-29 on dataforseo.com/pricing/google-serp/google-organic-serp-api and serpapi.com/pricing. A DataForSEO SERP is 10 results. SerpApi counts successful searches only.
Provider and modePublished price10,000 SERPsTurnaround as published
Searlo, Scale pack rate3 credits per page at $0.30 per 1,000 credits$9.00Sync: when the batch finishes; async: queued
Searlo, Enterprise pack rate3 credits per page at $0.20 per 1,000 credits$6.00As above
DataForSEO standard queue, normal priority$0.0006 per SERP$6.00About 5 minutes on average
DataForSEO standard queue, high priority$0.0012 per SERP$12.00Up to 1 minute on average
DataForSEO live mode$0.002 per SERP$20.00Up to 6 seconds on average
SerpApi Production plan$150 a month for 15,000 searches$150.00 (monthly plan)Not published

At the same one-page depth, DataForSEO's standard queue matches Searlo's lowest pack rate and undercuts the Scale rate, while its live mode costs more. Searlo's 3 credits cover the whole page (organic, ads, videos, People Also Ask, related searches, knowledge graph and, on request, the AI Overview), and credits are prepaid packs rather than a subscription. Prices change, so check each vendor's page before you buy.

What teams run in bulk

  • Rank snapshots: a keyword set per market each night, stored as your own history.
  • SERP-feature audits: which keywords show ads, People Also Ask, videos, a knowledge graph or an AI Overview.
  • Question research: People Also Ask and related searches across hundreds of seed keywords.
  • Competitor coverage: which domains hold the top positions across a category's keywords.
  • Market comparisons: the same keyword list in several countries, or on mobile against desktop.
  • Datasets: SERP snapshots for research or model evaluation, collected overnight by async tasks.

Related endpoints and pages

Honest limits

What the bulk endpoints do not do, so you can plan around them.

  • A synchronous batch is capped at 20 keywords and holds the connection open while it runs. Keywords are fetched a few at a time, so a full batch takes a while; use async tasks for anything larger.
  • In a synchronous batch a failed keyword is still billed, because the batch as a whole succeeds. Its row says why and whether it is retryable, so re-run those keywords. Failed single requests and failed async tasks are refunded.
  • Back-pressure is real. When the SERP lane is busy a request returns 429 with a retry-after; wait that long, because immediate retries are refused. Async tasks retry on their own.
  • Async results are not instant. Tasks run in priority order as workers become free, and each result is kept only until its expiresAt.
  • In async tasks, target a city with uule. A location name is resolved to a uule only on the synchronous endpoints.
  • Page features come from page one. Extra pages add organic results only, and each extra page is another 3 credits per keyword.

FAQ

Bulk SERP API: FAQ

How many keywords can I send at once?

Up to 20 per POST /serp/batch request, returned synchronously. For more, POST /async/task/batch accepts up to 500 tasks per call, and you can make as many calls as your credits cover.

How are bulk SERP requests billed?

3 credits per keyword per results page. In /serp/batch, duplicate keywords (ignoring case and surrounding spaces) are billed once, and every distinct keyword is billed even if its row fails. Async tasks hold 3 credits per page when queued, keep them when the task completes, and refund them when it fails or is cancelled.

When should I use async tasks instead of /serp/batch?

When the list is longer than 20 keywords, when you do not want to hold a connection open, or when you want per-keyword refunds on failure. Use /serp/batch when you need a small set of results back in one response.

Can I get results by webhook?

Yes, for async tasks. Add webhook.url (HTTPS) and optionally webhook.secret to each task; Searlo POSTs the finished task there and signs it with an HMAC-SHA256 in X-Searlo-Signature when a secret is set. Failed deliveries are retried for up to about 15 hours.

Can I set country, city and device?

Yes. A /serp/batch request takes one gl, hl, device and location (or uule) for all its keywords. Async tasks take them per task, but a city has to be given as uule there, because location names are only resolved on the synchronous endpoints.

Does each row include People Also Ask and AI Overviews?

Every successful row is a full SERP with organic results, ads, videos, People Also Ask, related searches and the knowledge graph. The AI Overview is off by default in /serp/batch; pass "aioverview": true to include it at no extra credit cost.

Is there a cheaper option if I only need organic results?

Yes. GET /search/web returns organic results only for 1 credit per query, and POST /search/batch runs up to 10 of those in one request. The Web Search API page covers both.

Is Searlo affiliated with Google?

No. Searlo is an independent service and is not affiliated with, endorsed by or sponsored by Google. Google is a trademark of Google LLC. Searlo returns publicly visible results as JSON.

Run your first batch free

3,000 free credits cover 1,000 keyword SERPs. No card, no subscription.