```yaml
product: AlterLab
title: "SERP API Reference — Search Results with Ad Extraction"
comparison_context: "AlterLab is an alternative to Firecrawl, ScrapingBee, and Bright Data."
source_url: https://alterlab.io/docs/api/serp
```

API Reference

New

# 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](https://alterlab.io/docs/migrations/search-to-serp). 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

1

### Query

POST a search query to `/api/v1/serp`. Specify engine, geo, device, and ad extraction options.

2

### Render

The SERP page is rendered in a browser so ads and rich features appear as they do for a person using the engine.

3

### 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](https://alterlab.io/docs/reference/changelog#schema-versioning).

## Quick Start

Get your first SERP result in seconds. This example queries Google for "crm software" with ad extraction enabled.

Bash

```
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

POST

`/api/v1/serp`

Execute 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.

JSON

```
{
  "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.

GET

`/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. |

Python

```
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

POST

`/api/v1/serp/batch`

Submit 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. |

Python

```
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

GET

`/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. |

JSON

```
{
  "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 |
| --- | --- | --- | --- | --- | --- |
| Google | $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.

Python

```
# 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

The `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.

Python

```
# 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 results
- `sidebar` — 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.

Python

```
# 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

Sessions persist as long as they are actively used. A session expires after 10 minutes of inactivity. Reusing an expired session_id transparently creates a new session.

### 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.

JSON

```
{
  "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.

Python

```
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.

Python

```
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.

Python

```
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.

Python

```
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

Every response includes a `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. |

Last updated: June 2026