SERP API
One endpoint for Google, Bing and DuckDuckGo search results: organic results, ads, rich snippets and structured data, selected per request with search_engine (alias engine).
Single search surface
/v1/serp is the single search endpoint. The older /v1/search and /v1/search/batch endpoints still work as deprecated aliases with identical billing; their responses carry a Deprecation header and a link to this endpoint. See the migration guide. This endpoint defaults to google ($0.004/query) when search_engine is omitted; pick duckduckgo ($0.001/query) for lightweight keyword search. Responses are structured JSON; scrape_results, domain and time_range are currently served only by the /v1/search alias.Overview
Query
POST a search query to /api/v1/serp. Specify engine, geo, device, and ad extraction options.
Render
The SERP page is rendered in a browser so ads and rich features appear as they do for a person using the engine.
Extract
Structured data is extracted: organic results, paid ads (with placement, advertiser, extensions), featured snippets, knowledge panels, and more.
| Feature | /v1/search | /api/v1/serp |
|---|---|---|
| Organic results | Yes | Yes |
| Paid ad extraction | No | Yes |
| Shopping ads | No | Yes |
| Brand classification | No | Yes |
| Session persistence | No | Yes |
| Challenge resolution | No | Yes |
| Rich results (PAA, KP) | Limited | Full |
| Cost per query | $0.001 | $0.004 |
Schema Versioning
Every response carries a schema_version field (currently "1"). It changes only on a breaking change; additive fields never bump it. Removals follow a minimum 180-day deprecation window announced in the schema changelog.
Quick Start
Get your first SERP result in seconds. This example queries Google for "crm software" with ad extraction enabled.
curl -X POST https://api.alterlab.io/api/v1/serp \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "crm software",
"include_ads": true,
"country": "US"
}'POST /api/v1/serp
/api/v1/serpExecute a SERP scrape with browser rendering. Returns organic results, paid ads, and rich result data. Supports inline wait (up to 30s) or async polling.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| query | string | Required | Search query (1-500 characters). The search term to look up on the target engine. |
| search_engine | "google" | "bing" | "duckduckgo" | Optional | Target search engine (alias: engine). Ad extraction is available on Google and Bing.Default: google |
| device | "desktop" | "mobile" | Optional | Device type to emulate. Affects SERP layout and ad placements. Desktop typically shows more ads.Default: desktop |
| country | string | Optional | ISO 3166-1 alpha-2 country code for geo-targeted results (e.g. "US", "GB", "DE"). Results are localized to that country. |
| city | string | Optional | City name for local ad targeting (e.g. "London", "New York"). Used with country for precise geo-targeting. |
| language | string | Optional | Language code for results and browser locale (e.g. "en", "de", "fr"). |
| num_results | integer | Optional | Number of organic results to request (1-100). Google typically serves 10 per page.Default: 10 |
| page | integer | Optional | Result page number (1-10). Page 2 = results 11-20.Default: 1 |
| include_ads | boolean | Optional | Extract ad data from the SERP. When false, only organic results are returned (faster). The primary use case for this endpoint.Default: true |
| brand_domain | string | Optional | Brand domain for ad classification (e.g. "wayfair.com"). Each ad gets is_brand_ad: true/false based on whether its display domain matches. |
| resolve_redirects | boolean | Optional | Follow ad click-through redirect chains to resolve final landing URLs. Adds ~200-500ms per ad.Default: false |
| session_id | string | Optional | Reuse a long-lived browser session across queries for consistent results. |
| solve_captchas | boolean | Optional | Automatically solve CAPTCHAs when Google presents a challenge. Adds latency but avoids errors.Default: true |
Response Schema
The response contains structured data extracted from the rendered SERP page. All fields are populated based on what the search engine returns.
{
"serp_id": "serp_abc123def456",
"query": "crm software",
"search_engine": "google",
"device": "desktop",
"country": "US",
"language": "en",
"session_id": null,
"organic_results": [
{
"position": 1,
"url": "https://www.salesforce.com/crm/",
"title": "CRM Software from Salesforce - Customer Relationship Management",
"snippet": "Salesforce CRM helps businesses of all sizes manage customer relationships...",
"displayed_url": "salesforce.com/crm",
"date_published": null,
"sitelinks": [
{ "title": "Pricing", "url": "https://www.salesforce.com/crm/pricing/" },
{ "title": "Free Trial", "url": "https://www.salesforce.com/form/signup/" }
]
}
],
"organic_count": 10,
"ads": [
{
"position": 1,
"placement": "top",
"ad_type": "text",
"headline": "HubSpot CRM - Free CRM Software",
"headline_2": "Start Growing Better Today",
"headline_3": null,
"description": "Get started with free CRM tools for your whole team. No credit card required.",
"description_2": null,
"display_url": "hubspot.com/crm/free",
"landing_url": null,
"tracking_url": "https://www.google.com/aclk?sa=l&ai=...",
"advertiser_name": "HubSpot",
"extensions": [
{ "type": "sitelink", "text": "Free Tools", "url": "https://hubspot.com/free" },
{ "type": "callout", "text": "No Credit Card Required", "url": null }
],
"is_brand_ad": false,
"is_sponsored": true,
"price": null,
"merchant": null,
"image_url": null,
"rating": null,
"review_count": null
}
],
"ads_count": 4,
"ads_by_placement": { "top": 3, "bottom": 1, "sidebar": 0, "shopping": 0 },
"featured_snippet": {
"content": "CRM software helps businesses manage customer interactions and data...",
"source_url": "https://example.com/what-is-crm",
"source_title": "What is CRM? - Complete Guide",
"snippet_type": "text"
},
"knowledge_panel": null,
"people_also_ask": [
{
"question": "What is the best CRM software for small businesses?",
"answer": "Popular CRM options for small businesses include HubSpot CRM (free tier)...",
"source_url": "https://example.com/best-crm"
}
],
"related_searches": [
"best crm software 2026",
"free crm software",
"crm software for small business"
],
"did_you_mean": null,
"local_results": [],
"video_results": [],
"news_results": [],
"image_results": [],
"shopping_results": [],
"estimated_total_results": 2340000000,
"cost_breakdown": {
"base_cost_microcents": 4000,
"ad_extraction_cost_microcents": 0,
"captcha_solve_cost_microcents": 0,
"total_cost_microcents": 4000,
"tier_used": "4"
},
"captcha_stats": null
}Ad Result Schema
Each ad in the ads array contains the full ad creative with positioning, text, URLs, and extensions.
| Field | Type | Description |
|---|---|---|
| position | integer | Ad position within its placement group (1-indexed) |
| placement | "top" | "bottom" | "sidebar" | "shopping" | Where the ad appears on the SERP page |
| ad_type | "text" | "shopping" | "local" | "video" | "app" | Ad format type |
| headline | string | Primary ad headline text |
| headline_2, headline_3 | string | null | Additional headlines if present |
| description | string | null | Ad description / body text |
| display_url | string | URL displayed in the ad (breadcrumb-style) |
| landing_url | string | null | Final landing URL after redirect resolution (requires resolve_redirects: true) |
| tracking_url | string | null | Raw click-through URL from the search engine's tracking redirect |
| advertiser_name | string | null | Verified advertiser name shown by Google |
| extensions | array | Sitelinks, callouts, phone numbers, and other extensions |
| is_brand_ad | boolean | null | Whether the ad's display domain matches the brand_domain. Null when brand_domain not provided. |
| price | string | null | Product price — shopping ads only |
| merchant | string | null | Merchant name — shopping ads only |
Organic Result Schema
| Field | Type | Description |
|---|---|---|
| position | integer | 1-indexed position on the page |
| url | string | Result URL |
| title | string | Result title |
| snippet | string | null | Result snippet/description |
| displayed_url | string | null | URL as displayed in SERP (breadcrumb-style) |
| date_published | string | null | Date shown next to the result |
| sitelinks | array | Sitelinks under this result (title + url) |
Rich Results
The response includes all rich SERP features when present on the page.
| Field | Type | Description |
|---|---|---|
| featured_snippet | object | null | Featured snippet / answer box (content, source_url, snippet_type) |
| knowledge_panel | object | null | Knowledge panel (title, entity_type, description, facts) |
| people_also_ask | array | PAA questions (question, answer, source_url) |
| related_searches | array of strings | Related search suggestions |
| local_results | array | Local pack results (name, address, rating, phone) |
| video_results | array | Video carousel (title, url, source, duration) |
| news_results | array | News carousel (title, url, source, date) |
| shopping_results | array | Organic shopping carousel (title, price, merchant) |
Cost Breakdown
Every response includes a cost_breakdown object showing exactly what was charged.
| Field | Type | Description |
|---|---|---|
| base_cost_microcents | integer | Base cost including browser rendering (4000 = $0.004) |
| ad_extraction_cost_microcents | integer | Additional cost for redirect resolution |
| captcha_solve_cost_microcents | integer | Challenge resolution cost (non-zero when a challenge was resolved) |
| total_cost_microcents | integer | Total cost for this request |
| tier_used | string | Scraping tier used (always "4" for SERP) |
Async Polling
If the SERP scrape takes longer than 30 seconds (e.g., due to CAPTCHA solving), the response includes a serp_id with status "processing". Poll for results using the status endpoint.
/api/v1/serp/{serp_id}Poll for async SERP results. Returns the full SerpResponse when status is 'completed'.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| serp_id | string | Required | The serp_id from the initial POST /api/v1/serp response. |
import time
import requests
# Initial request
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"query": "insurance quotes", "solve_captchas": True},
)
data = resp.json()
# If processing, poll for results
if data.get("status") == "processing":
serp_id = data["serp_id"]
while True:
time.sleep(2)
status = requests.get(
f"https://api.alterlab.io/api/v1/serp/{serp_id}",
headers={"X-API-Key": "YOUR_API_KEY"},
).json()
if status["status"] == "completed":
data = status["result"]
break
elif status["status"] == "failed":
raise Exception(f"SERP scrape failed: {status.get('error')}")Batch SERP
Submit up to 50 SERP queries for parallel processing. Ideal for monitoring multiple keywords or comparing results across markets.
Batch Request
/api/v1/serp/batchSubmit a batch of SERP queries for async parallel processing. Each query can override batch-level defaults.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| queries | array | Required | Array of query objects (1-50 items). Each item has: query (required), device, country, city, language, include_ads, brand_domain. |
| search_engine | "google" | "bing" | "duckduckgo" | Optional | Default search engine for all queries.Default: google |
| device | "desktop" | "mobile" | Optional | Default device type for all queries.Default: desktop |
| country | string | Optional | Default country for all queries. |
| include_ads | boolean | Optional | Default ad extraction setting.Default: true |
| brand_domain | string | Optional | Default brand domain for classification. |
| session_id | string | Optional | Reuse a session for all queries in this batch. |
| webhook_url | string | Optional | Webhook URL for result delivery. Results are POSTed as each query completes. |
import requests
resp = requests.post(
"https://api.alterlab.io/api/v1/serp/batch",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"queries": [
{"query": "crm software", "country": "US"},
{"query": "crm software", "country": "GB"},
{"query": "crm software", "country": "DE"},
],
"include_ads": True,
"brand_domain": "salesforce.com",
},
)
batch = resp.json()
print(f"Batch ID: {batch['batch_id']}")
cost = batch['estimated_cost_microcents'] / 1_000_000
print(f"Estimated cost: {cost:.4f}")Poll Batch Status
/api/v1/serp/batch/{batch_id}Poll for batch progress and results. Returns per-query status and results as they complete.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| batch_id | string | Required | Batch ID from the POST /api/v1/serp/batch response. |
{
"batch_id": "batch_xyz789",
"status": "completed",
"total_queries": 3,
"completed": 3,
"failed": 0,
"total_cost_microcents": 12000,
"items": [
{
"index": 0,
"query": "crm software",
"status": "completed",
"result": { "...full SerpResponse..." },
"error": null
}
]
}Configuration Guide
Engine Selection
Choose the engine per request with search_engine. Each engine has a flat per-query price, listed below and in Pricing. Pagination uses num_results (alias num) and start (or page); the window start + num_results may not exceed the engine cap (DuckDuckGo 30, Google 100, Bing 100) or the request is rejected with 422 before any charge. device (desktop or mobile) selects the result layout.
| Engine | Price | Ad Extraction | Shopping Ads | Rich Results | Geo-Targeting |
|---|---|---|---|---|---|
| $0.004 | Full (top, bottom, sidebar) | Yes | Full (PAA, KP, snippets, local, video, news) | Country + city | |
| Bing | $0.004 | Top + sidebar | Limited | Basic (snippets, PAA) | Country only |
| DuckDuckGo | $0.001 | Not available | Not available | Organic results | Country |
Geo-Targeting
Control where your search originates from using country and city parameters. Results are localized to the specified location.
# Compare ads across markets
markets = [
{"country": "US", "language": "en"},
{"country": "GB", "language": "en"},
{"country": "DE", "language": "de"},
{"country": "FR", "language": "fr"},
]
for market in markets:
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"query": "project management software", **market, "include_ads": True},
)
data = resp.json()
print(f"{market['country']}: {data['ads_count']} ads found")City requires Country
city parameter only works when country is also provided. Without a country, city targeting is ignored.Ad Extraction
When include_ads: true (default), the SERP API extracts all paid ad placements. Use brand_domain to automatically classify ads as brand vs. competitor.
# Monitor who's bidding on your brand keywords
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"query": "salesforce crm",
"brand_domain": "salesforce.com",
"include_ads": True,
"resolve_redirects": True, # Get actual landing URLs
},
)
data = resp.json()
brand_ads = [ad for ad in data["ads"] if ad["is_brand_ad"] is True]
competitor_ads = [ad for ad in data["ads"] if ad["is_brand_ad"] is False]
print(f"Brand ads: {len(brand_ads)}")
print(f"Competitor ads: {len(competitor_ads)}")
for ad in competitor_ads:
print(f" - {ad['advertiser_name'] or ad['display_url']}: {ad['headline']}")Ad Placement Types
top— Above organic results (highest visibility, highest CPC)bottom— Below organic resultssidebar— Right-side panel (desktop only)shopping— Shopping carousel (product listings with images/prices)
Session Management
Use session_id to maintain a persistent browser session across multiple queries. The same browser session is reused, which keeps pagination and results consistent.
# Paginate through results on the same session
session_id = "my-research-session-001"
all_results = []
for page in range(1, 4): # Pages 1-3
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"query": "best project management tools",
"session_id": session_id,
"page": page,
"include_ads": True,
},
)
data = resp.json()
all_results.extend(data["organic_results"])
print(f"Page {page}: {len(data['organic_results'])} results, {data['ads_count']} ads")
print(f"Total organic results collected: {len(all_results)}")Session Lifespan
Challenge Resolution
A search engine may present a verification challenge. With solve_captchas: true (default), challenges are resolved transparently. The response includes captcha_stats when a solve occurred.
{
"captcha_stats": {
"captcha_encountered": true,
"captcha_solved": true,
"solve_time_ms": 4200
}
}Use Cases
Basic SERP Scraping
Get organic results for a keyword with full metadata.
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"query": "best laptops 2026", "include_ads": False},
)
for result in resp.json()["organic_results"]:
print(f"{result['position']}. {result['title']}")
print(f" {result['url']}")
print(f" {result['snippet']}")
print()Ad Intelligence
Extract all advertisers bidding on a keyword with placement data.
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"query": "project management software",
"include_ads": True,
"resolve_redirects": True,
},
)
data = resp.json()
print(f"Ads by placement: {data['ads_by_placement']}")
for ad in data["ads"]:
print(f"[{ad['placement']}#{ad['position']}] {ad['headline']}")
print(f" Advertiser: {ad['advertiser_name']}")
print(f" Display: {ad['display_url']}")
print(f" Landing: {ad['landing_url']}")
print(f" Extensions: {len(ad['extensions'])}")
print()Brand vs. Competitor Monitoring
Detect who is advertising on your brand keywords and classify brand vs. competitor placements.
keywords = ["wayfair furniture", "wayfair couches", "wayfair sale"]
competitor_data = []
for kw in keywords:
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={
"query": kw,
"brand_domain": "wayfair.com",
"include_ads": True,
},
)
data = resp.json()
competitors = [ad for ad in data["ads"] if ad["is_brand_ad"] is False]
competitor_data.append({"keyword": kw, "competitors": competitors})
# Report on competitor activity
for item in competitor_data:
print(f"\nKeyword: {item['keyword']}")
for ad in item["competitors"]:
print(f" Competitor: {ad['display_url']} - {ad['headline']}")Geo-Targeted Comparison
Compare SERP results and ad landscape across different markets.
import requests
markets = ["US", "GB", "DE", "JP", "BR"]
query = "cloud computing services"
for country in markets:
resp = requests.post(
"https://api.alterlab.io/api/v1/serp",
headers={"X-API-Key": "YOUR_API_KEY"},
json={"query": query, "country": country, "include_ads": True},
)
data = resp.json()
top_organic = data["organic_results"][0]["title"] if data["organic_results"] else "N/A"
print(f"{country}: {data['organic_count']} organic, {data['ads_count']} ads")
print(f" Top result: {top_organic}")
print(f" Ads breakdown: {data['ads_by_placement']}")Pricing
| Component | Cost | Notes |
|---|---|---|
| DuckDuckGo query | $0.001 / query | Default engine on the deprecated /v1/search alias |
| Google query | $0.004 / query | Default engine on /v1/serp; includes ad extraction and all SERP features |
| Bing query | $0.004 / query | Includes ad extraction |
| Challenge resolution | Bundled | Included in base cost when solve_captchas is enabled |
| Redirect resolution | Bundled | Included when resolve_redirects is enabled |
| Batch (up to 50 queries) | Per-engine price x N | Same per-query pricing, parallel execution |
Cost Transparency
cost_breakdown object showing the exact cost charged. Monitor your spend in real-time via the total_cost_microcents field (1,000,000 microcents = $1).Example cost calculations
- 1,000 Google or Bing queries/day = $4.00/day = $120/month
- 10,000 Google or Bing queries/day = $40.00/day = $1,200/month
- 100,000 Google or Bing queries/day = $400.00/day = $12,000/month
Error Codes
| Code | Meaning | Resolution |
|---|---|---|
| 400 | Invalid request parameters | Check query length, country code format, parameter types |
| 401 | Invalid or missing API key | Verify X-API-Key header is present and valid |
| 402 | Insufficient balance | Add funds to your account. Per-query prices are listed in Pricing. |
| 422 | Validation error | Check the error detail — often an unsupported country code or invalid query |
| 429 | Rate limited | Back off and retry after the Retry-After header. Rate limits are per API key and match scrape requests; a batch counts one request per query. |
| 500 | Internal error | Retry after a few seconds. If persistent, check status page. |
| 503 | SERP scrape failed (blocked/timeout) | The search engine did not return results. Retrying may succeed. |