# Google SERP & AEO Scraper (`morph_coder/google-serp-aeo-scraper`) Actor

Scrape Google organic SERP with geo targeting, search operators, and flat SEO export. Match Google ranks vs ChatGPT and other LLM citations in one dataset. Includes a Google Sheets template — run scans from a spreadsheet, no code required.

- **URL**: https://apify.com/morph\_coder/google-serp-aeo-scraper.md
- **Developed by:** [Morph Coder](https://apify.com/morph_coder) (community)
- **Categories:** SEO tools, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.26 / 1,000 organic-result-scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Google SERP & AEO Scraper

Apify Actor for automated Google Search scraping with **GOOGLE_SERP proxy**, manual search operators, country targeting, ads detection, **flat SEO export**, and optional **LLM visibility comparison**.

**Differentiator:** includes a **Google Sheets workflow** — run SERP + AEO scans from a spreadsheet (keywords, settings, results, run cost) with **no coding** after a 5-minute setup.

### Google Sheets workflow

Run this Actor from Google Sheets — most users never touch the API.

| | |
|--|--|
| **Copy template (fastest)** | [Make a copy](https://docs.google.com/spreadsheets/d/11KLOuIvrZd6Md4GlLa56aNez_TilCqym6t5jH3gTyUw/copy) |
| **Install guide** | [morph-coder/google-serp-aeo-scraper](https://github.com/morph-coder/google-serp-aeo-scraper) |

**Quick start**

1. [Activate the Actor](https://apify.com/morph_coder/google-serp-aeo-scraper) in your Apify account.
2. Copy the [template spreadsheet](https://docs.google.com/spreadsheets/d/11KLOuIvrZd6Md4GlLa56aNez_TilCqym6t5jH3gTyUw/copy) → **SERP Tools → Configure Apify token**.
3. Add keywords on the **Keywords** sheet → **SERP Tools → Run SERP scan** → results land in **Results**, **LLM Summary**, and **Run Log** (`costUsd` per run).

Prefer your own spreadsheet? Paste [`Code.gs`](https://raw.githubusercontent.com/morph-coder/google-serp-aeo-scraper/main/code.gs) via **Extensions → Apps Script** — step-by-step in the [Sheets install guide](https://github.com/morph-coder/google-serp-aeo-scraper). Review the script with your AI assistant before authorizing Google permissions.

#### Screenshots

**Results** — flat SERP rows (rank, URL, `resultsTotal`, LLM citation flags):

![Google Sheets Results tab](https://raw.githubusercontent.com/morph-coder/google-serp-aeo-scraper/main/sheets-results.png)

**LLM Summary** — visibility overlap and ranks per keyword:

![Google Sheets LLM Summary tab](https://raw.githubusercontent.com/morph-coder/google-serp-aeo-scraper/main/sheets-llm-summary.png)

**Run Log** — organic counts, LLM calls, and `costUsd` per run:

![Google Sheets Run Log tab](https://raw.githubusercontent.com/morph-coder/google-serp-aeo-scraper/main/sheets-run-log.png)

### Features

- Google SERP via [Apify GOOGLE_SERP proxy](https://docs.apify.com/platform/proxy/google-serp-proxy) + CheerioCrawler
- Search operators: `site:`, `related:`, `intitle:`, date filters, file types, exact match
- Country / language / UULE geo targeting (`countryCode` → `google.{tld}` + **GOOGLE_SERP proxy country**, with fallback to global SERP pool)
- Pagination: **`maxResultsPerQuery`** (per keyword), **`maxTotalResults`** (whole-run cap), or **`maxPagesPerQuery`** (fixed pages). See [Limit priority](#limit-priority) below.
- Output formats: **page** (Apify-compatible), **flat** (Sheets/Zapier), **both**
- Paid ads + shopping ads parsing, PAA, related queries, AI Overview
- Optional LLM add-ons (API): ChatGPT, Gemini, Perplexity, DeepSeek, Copilot
- **Google Sheets** — template + Apps Script ([workflow](#google-sheets-workflow))

### Input example

Single keyword, top-20:

```json
{
  "queries": ["NASA"],
  "countryCode": "us",
  "maxResultsPerQuery": 20,
  "outputFormat": "flat"
}
````

Multiple keywords (one dataset, filter by `keyword`):

```json
{
  "queries": ["NASA", "SpaceX", "Tesla"],
  "countryCode": "us",
  "maxResultsPerQuery": 20,
  "outputFormat": "flat"
}
```

→ up to **60 organic rows** (20 × 3 keywords). Leave **`maxTotalResults` unset** for full per-keyword depth.

#### Limit priority

When several limits are set, they apply in this order:

| Priority | Setting | Scope |
|----------|---------|--------|
| 1 | **`maxTotalResults`** | Hard cap on **all keywords combined**. When set, keywords run **one by one**; the first keyword(s) consume the budget. |
| 2 | **`maxResultsPerQuery`** | Target top-N **per keyword** (may be cut short if global cap runs out). |
| 3 | **`maxPagesPerQuery`** | Used only when `maxResultsPerQuery` is **not** set. |
| — | Empty SERP page (`organicOnCurrentPage === 0`) | No more rows — stop even if `start=` could advance. |
| — | Missing Google "Next" link | **Not a stop** when `maxResultsPerQuery` is set — actor continues via `start=` until target or empty page. |

**Example — 2 keywords, `maxResultsPerQuery: 20`, `maxTotalResults: 34`:**

```
nike   → 20 rows (uses 20 of 34 global budget)
adidas → 14 rows only (34 − 20 = 14 left; stops before top-20)
```

To get **20 + 20 = 40** rows, either **omit `maxTotalResults`** or set it **≥ 40** (e.g. `queries.length × maxResultsPerQuery`).

The run ends with `usage_summary.billing.organicByKeyword` so you can see per-keyword counts vs limits.

Cap total billing with **`maxTotalResults`** (optional):

```json
{
  "queries": ["kw1", "kw2", "kw3", "kw4", "kw5", "kw6", "kw7", "kw8", "kw9", "kw10"],
  "maxResultsPerQuery": 20,
  "maxTotalResults": 20,
  "outputFormat": "flat"
}
```

→ **20 rows total** (first keyword(s) until cap), not 200.

Legacy API: `queries` may also be a newline-separated string.

### Output

**Page record** — one row per SERP page with `organicResults[]`, `paidResults[]`, etc.

**Flat record** (`recordType: flat`) — one row per SERP result:

`keyword`, **`resultsTotal`** (Google estimate, e.g. *About 1,230,000,000 results* — same on all rows for that keyword), `rank`, `serpSlot`, `queryPage`, `position`, `title`, `url`, `type`, `isPaid`, `targetCountry`, `llm*Cited`, `llm*UrlRank`, …

| keyword | rank | serpSlot | queryPage | position | title | url | isPaid |
|---------|------|----------|-----------|----------|-------|-----|--------|

**LLM record** (`recordType: llm`) — one row per keyword/page when AI add-ons are enabled and `outputFormat` is `flat`. Contains `chatGptSearchResult`, `geminiSearchResult`, `visibilityCompare`, etc. Use dataset view **LLM answers** or `?view=llm_results`.

With `outputFormat: page` or `both`, LLM fields live on the page record instead.

- **`resultsTotal`** — Google’s estimated match count from the SERP header (*About X results*). Parsed from page 1 and copied to all flat rows for that keyword. Approximate — not the same as rows you scrape.
- **`rank`** — sequential 1…N for analytics (top-20, charts). No gaps.
- **`serpSlot`** — estimated Google slot `(page−1)×10+position`; may skip numbers when a page has <10 organic results.
- **`queryPage` + `position`** — where on the SERP page the result appeared.

API export (no flat rows needed):

```
GET /v2/datasets/{id}/items?format=json&fields=searchQuery,organicResults&unwind=organicResults
```

### Local development

```bash
npm install
npm run build
cp .env.example .env   # optional
npx apify run --input scripts/test-input.example.json
```

### Pay-per-event billing

**Pricing model:** Pay per event (active from **June 15, 2026**). **Dataset rows are free** — `apify-default-dataset-item` = **$0**.

Apify displays prices **per 1,000 events** in the Console. Below are the **published** rates from Monetization settings.

**Platform usage:** **User pays platform usage costs** (proxy, compute, memory) **on top of** PPE events. `usageTotalUsd` in Run Log includes both.

#### Competitive context

| Actor / tier | Unit | Effective $ / 1,000 URLs\* |
|--------------|------|----------------------------|
| [apify/google-search-scraper](https://apify.com/apify/google-search-scraper) | **$2.50 / 1,000 pages** | **~$0.25** (≈10 organic/page) |
| Budget scrapers on Store | per URL | **~$0.05–0.15** |
| **This Actor** (organic, base tier) | per delivered URL | **$0.40** |
| **This Actor** (Bronze Apify plan) | per delivered URL | **$0.30** |

\*Example: **50 keywords × top-20 = 1,000 URLs** → Apify scraper ≈ **$0.25** SERP; us **$0.40** (base) or **$0.30** (Bronze) **+ platform usage**.

| Model | Apify official scraper | This Actor |
|-------|---------------------|------------|
| SERP unit | Per **page** fetched | Per **organic URL** delivered |
| Sparse page (6 results) | Full page price | Only rows you get |
| Output | SERP JSON | Flat rows + optional **LLM citations** + visibility compare |

Organic pricing uses **Apify plan tier discounts** (Bronze / Silver / Gold). LLM add-ons are flat per provider.

#### Event prices (Console)

| Event | Price / 1,000 | Per unit | Notes |
|-------|-----------------|----------|-------|
| `organic-result-scraped` | **$0.40** (base) | **$0.0004** / URL | Tiered — see below |
| ↳ Bronze discount | **$0.30** | **$0.0003** / URL | Apify Bronze plan |
| ↳ Silver discount | **$0.28** | **$0.00028** / URL | Apify Silver plan |
| ↳ Gold discount | **$0.26** | **$0.00026** / URL | Apify Gold plan |
| `chatgpt-search-scraped` | **$25.00** | **$0.025** / call | Successful web search only |
| `gemini-search-scraped` | **$25.00** | **$0.025** / call | |
| `perplexity-search-scraped` | **$30.00** | **$0.03** / call | |
| `copilot-search-scraped` | **$25.00** | **$0.025** / call | |
| `deepseek-search-scraped` | **$20.00** | **$0.02** / call | **Experimental** — chat only, no web citations |
| `ads-scraped` | **$0.25** | **$0.00025** / ad | Only when `focusOnPaidAds: true` |
| `apify-actor-start` | — | **$0.0005** / run | Actor start event |

Failed LLM calls (missing API key, API error) are **not** charged.

#### Pricing examples

PPE only (base organic tier). **Add platform usage** on top — see `usageTotalUsd` in Run Log.

**Bulk SEO — 50 keywords, top-20 organic, SERP only** (1,000 flat rows)

```
1 × actor start        $0.0005
1000 × organic        $0.4000
──────────────────────────────
PPE subtotal          ≈ $0.40 / run     (Bronze organic ≈ $0.30; Apify scraper ≈ $0.25)
+ platform usage
```

**1 keyword, top-20 organic, ChatGPT + Gemini + Perplexity + DeepSeek** (~3 SERP pages fetched)

```
1 × actor start              $0.0005
20 × organic                 $0.0080
1 × ChatGPT                  $0.0250
1 × Gemini                   $0.0250
1 × Perplexity               $0.0300
1 × DeepSeek                 $0.0200
──────────────────────────────────────
PPE subtotal                 ≈ $0.11 / run
+ platform usage
```

**Light — 1 keyword, top-10 organic, ChatGPT only**

```
1 × actor start     $0.0005
10 × organic       $0.0040
1 × ChatGPT          $0.0250
─────────────────────────
PPE subtotal       ≈ $0.03 / run
+ platform usage
```

**Typical — 2 keywords, top-10 organic, ChatGPT**

```
1 × actor start     $0.0005
20 × organic       $0.0080
2 × ChatGPT          $0.0500
─────────────────────────
PPE subtotal       ≈ $0.06 / run
+ platform usage
```

**SEO batch — 5 keywords, top-20 organic, ChatGPT**

```
1 × actor start     $0.0005
100 × organic      $0.0400
5 × ChatGPT          $0.1250
─────────────────────────
PPE subtotal       ≈ $0.17 / run
+ platform usage
```

**Formula (PPE only, base organic tier):**

```
PPE cost ≈ $0.0005
         + (organic URLs × organic rate)     // $0.0004 base; $0.0003 Bronze; etc.
         + Σ(LLM calls × provider rate)      // ChatGPT/Gemini/Copilot $0.025; Perplexity $0.03; DeepSeek $0.02
         + (paid ads × $0.00025)             // if focusOnPaidAds
         + platform usage                    // proxy + compute (billed separately)
```

#### Margin notes (for Actor owner)

| Item | ~API cost | PPE price (base) | ~margin |
|------|-----------|------------------|---------|
| Organic URL | proxy/compute (platform → user) | $0.0004 | PPE margin; platform passed through |
| ChatGPT / Gemini / Copilot | ~$0.01/call | $0.025 | ~150% |
| Perplexity | ~$0.01/call | $0.03 | ~200% |
| DeepSeek | ~$0.01/call | $0.02 | ~100% |
| Dataset rows | — | $0 | Sheets workflow |

`usageTotalUsd` in Run Log (Sheets) and Apify Console is the source of truth for what the **user** paid (PPE + platform). API dashboards track **your** LLM COGS separately.

#### Google SERP — per organic URL

| Event | When | Count |
|-------|------|-------|
| `organic-result-scraped` | Each **organic** result row saved | 1 per URL |

You pay for **delivered organic URLs**, not for SERP pages fetched. Capped by `maxResultsPerQuery` and `maxTotalResults`.

**Estimate:**

```
SERP PPE ≈ organic URLs × your tier rate   // base $0.0004; Bronze $0.0003; Silver $0.00028; Gold $0.00026
           + platform usage
```

#### Paid ads (optional)

| Event | When |
|-------|------|
| `ads-scraped` | Each paid ad row when `focusOnPaidAds: true` |

#### LLM add-ons — per API call

| Event | When | Count |
|-------|------|-------|
| `chatgpt-search-scraped` | Successful ChatGPT web search | 1 per keyword (`perQuery`) or per SERP page (`perPage`) |
| `gemini-search-scraped` | Successful Gemini call (with citations) | same |
| `perplexity-search-scraped` | Successful Perplexity call | same |
| `deepseek-search-scraped` | Successful DeepSeek chat completion | same — **experimental** |
| `copilot-search-scraped` | Successful Copilot call | same |
| `ai-mode-result-scraped` | AI Mode add-on | not implemented — **do not charge** |

LLM billing is **per request**, not per citation. Failed calls are not charged.

**Estimate:**

```
LLM PPE ≈ Σ(successful calls × provider rate)
         // ChatGPT / Gemini / Copilot $0.025; Perplexity $0.03; DeepSeek $0.02
```

#### Run audit row

The dataset ends with `recordType: usage_summary`:

```json
{
  "billing": {
    "organicUrlsCharged": 17,
    "llmCallsCharged": { "chatGpt": 2 },
    "pagesFetched": 4
  },
  "llmTokenUsage": { "...": "provider token totals for your margin" }
}
```

`pagesFetched` is informational only — not used for client billing.

### LLM secrets (Actor environment variables)

| Variable | Provider |
|----------|----------|
| `OPENAI_API_KEY` | ChatGPT (Responses API + `web_search`) |
| `GEMINI_API_KEY` | Gemini |
| `PERPLEXITY_API_KEY` | Perplexity |
| `DEEPSEEK_API_KEY` | DeepSeek |
| `AZURE_OPENAI_*` | Copilot |

ChatGPT uses **OpenAI Responses API** with the hosted **`web_search`** tool. Gemini uses **`google_search` grounding** on the Generative Language API (`gemini-2.5-flash` by default). Set `OPENAI_MODEL` / `GEMINI_MODEL` in Actor secrets.

**DeepSeek (experimental):** plain chat completion only — lowest API token cost, but **no web search or citation URLs** yet. Useful for testing narrative/brand mentions; not recommended for AEO rank tracking until a search API is added. Priced at **$0.02/call** ($20 / 1,000) in Console.

#### LLM input options

| Field | Default | Purpose |
|-------|---------|---------|
| `llmQueryMode` | `sameAsKeyword` | Send exact Google keyword to LLM web search |
| `llmApplyTo` | `brandLike` | Skip commercial/long-tail keywords for LLM |
| `llmSearchScope` | `perQuery` | One LLM call per keyword (page 1) |

`visibilityCompare.rankedComparison` maps **googleRank** vs **llmRank** per URL. **`rankedComparisonByDomain`** uses the best rank per domain (handles different paths and `utm_*` params). **`targetDomainRanks`** defaults to Google #1 domain when `targetDomains` is not set.

Each LLM result includes **`usage`** (`inputTokens`, `outputTokens`, `totalTokens`, optional `webSearchCalls`) and **`model`** for your internal margin tracking. See **`usage_summary.billing`** for PPE totals per run.

Clients do not pass API keys in input — LLM API cost is included in LLM PPE events.

### Google Sheets (more detail)

The [Google Sheets workflow](#google-sheets-workflow) section at the top covers copy-template setup. Additional notes:

- Apify token is stored in **Script Properties**, not in cells — sent only to `api.apify.com`.
- **Results** / LLM sheets refresh each run; **Run Log** is append-only history with **`costUsd`** (`usageTotalUsd` from Apify).

| Sheet | Contents |
|-------|----------|
| **Results** | Flat organic SERP + LLM citation columns per URL |
| **LLM Summary** | Visibility KPIs + citation counts per provider |
| **LLM Answers** | Full LLM answer text (one row per keyword × provider) |
| **LLM Citations** | Parsed citations (`rank`, `url`, `title`) |
| **Run Log** | Organic counts, LLM calls, **`costUsd`** per run |

Install from scratch: [integrations/google-sheets/README.md](integrations/google-sheets/README.md).

### Disclaimer

Use responsibly. Comply with [Google Terms of Service](https://policies.google.com/terms) and applicable laws.

# Actor input Schema

## `queries` (type: `array`):

One or more keywords or Google Search URLs. All results go to one dataset; use the keyword column to filter. API clients may also pass a newline-separated string.

## `countryCode` (type: `string`):

ISO 3166-1 alpha-2 country code: selects Google domain (google.co.in, …), GOOGLE\_SERP proxy country (with fallback), and default UI language.

## `maxPagesPerQuery` (type: `integer`):

Stop after this many SERP pages when maxResultsPerQuery is not set (1–10). Ignored when maxResultsPerQuery is set — the actor fetches extra pages until the organic target is met.

## `maxResultsPerQuery` (type: `integer`):

Top-N organic rows for each keyword (e.g. 20 = top-20 per keyword). With 10 keywords this can mean up to 200 rows — use maxTotalResults to cap the whole run.

## `maxTotalResults` (type: `integer`):

Optional hard cap on total organic rows across all keywords. When set, keywords are processed sequentially and the budget is consumed in order — e.g. maxTotalResults 34 with two keywords at top-20 gives 20 + 14 rows. Omit for full maxResultsPerQuery on every keyword.

## `outputFormat` (type: `string`):

page = one row per SERP page; flat = one row per result; both = both formats.

## `mobileResults` (type: `boolean`):

Scrape mobile SERP layout instead of desktop.

## `forceExactMatch` (type: `boolean`):

Wrap each query in double quotes.

## `site` (type: `string`):

Restrict results to a domain (site: operator).

## `relatedToSite` (type: `string`):

Find pages related to a domain (related: operator). Ignored if site is set.

## `wordsInTitle` (type: `array`):

Add intitle: filters.

## `wordsInText` (type: `array`):

Add intext: filters.

## `wordsInUrl` (type: `array`):

Add inurl: filters.

## `fileTypes` (type: `array`):

Restrict to file types (pdf, doc, xls, ppt, etc.).

## `quickDateRange` (type: `string`):

Relative date filter: d (1 day), d10, w2, m6, y1.

## `beforeDate` (type: `string`):

Absolute date filter (YYYY-MM-DD).

## `afterDate` (type: `string`):

Absolute date filter (YYYY-MM-DD).

## `searchLanguage` (type: `string`):

Restrict result language (Google lr parameter).

## `languageCode` (type: `string`):

Google interface language.

## `locationUule` (type: `string`):

Encoded geolocation (uule parameter).

## `focusOnPaidAds` (type: `boolean`):

Enhanced ad extraction with up to 3 retries per page.

## `includeUnfilteredResults` (type: `boolean`):

Include lower-quality or duplicate results that Google normally filters out.

## `saveHtml` (type: `boolean`):

Store raw SERP HTML in each dataset row (large payloads).

## `saveHtmlToKeyValueStore` (type: `boolean`):

Save HTML snapshots to the run key-value store and link via htmlSnapshotUrl.

## `includeIcons` (type: `boolean`):

Embed base64 favicon images in result objects.

## `disableGoogleSearchResults` (type: `boolean`):

Skip Google SERP scraping and run LLM add-ons only.

## `targetBrand` (type: `string`):

Brand name for visibility comparison in LLM results.

## `targetDomains` (type: `array`):

Domains to track in results.

## `llmSearchScope` (type: `string`):

perQuery = one LLM call per keyword; perPage = call on each SERP page.

## `llmQueryMode` (type: `string`):

sameAsKeyword sends the exact Google keyword to the LLM web search (recommended). conversational expands short keywords into a question.

## `llmApplyTo` (type: `string`):

brandLike = short brand-style keywords only (default). targetOnly = keywords matching targetBrand/targetDomains. all = every keyword.

## `aiModeSearch` (type: `object`):

Google AI Mode (Playwright UI — coming soon).

## `chatGptSearch` (type: `object`):

Fetch AI answer via OpenAI Responses API with web search (requires OPENAI\_API\_KEY secret).

## `copilotSearch` (type: `object`):

Fetch AI answer via Azure OpenAI (requires AZURE\_OPENAI\_\* secrets).

## `geminiSearch` (type: `object`):

Fetch AI answer via Gemini API (requires GEMINI\_API\_KEY secret).

## `perplexitySearch` (type: `object`):

Fetch AI answer via Perplexity API (requires PERPLEXITY\_API\_KEY secret).

## `deepSeekSearch` (type: `object`):

Experimental: DeepSeek chat completion only (no web citations). $0.02/call ($20 / 1,000). Requires DEEPSEEK\_API\_KEY in Actor secrets.

## Actor input object example

```json
{
  "queries": [
    "NASA",
    "SpaceX",
    "Tesla"
  ],
  "countryCode": "us",
  "outputFormat": "both",
  "mobileResults": false,
  "forceExactMatch": false,
  "focusOnPaidAds": false,
  "includeUnfilteredResults": false,
  "saveHtml": false,
  "saveHtmlToKeyValueStore": true,
  "includeIcons": false,
  "disableGoogleSearchResults": false,
  "llmSearchScope": "perQuery",
  "llmQueryMode": "sameAsKeyword",
  "llmApplyTo": "brandLike"
}
```

# Actor output Schema

## `results` (type: `string`):

All dataset items from this run (default SERP pages view).

## `seoFlat` (type: `string`):

Flat SEO rows with rank, title, URL, and paid flag.

## `llmResults` (type: `string`):

ChatGPT, Gemini, and other AI add-on results (when outputFormat is flat).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "NASA",
        "SpaceX",
        "Tesla"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("morph_coder/google-serp-aeo-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "queries": [
        "NASA",
        "SpaceX",
        "Tesla",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("morph_coder/google-serp-aeo-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "NASA",
    "SpaceX",
    "Tesla"
  ]
}' |
apify call morph_coder/google-serp-aeo-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=morph_coder/google-serp-aeo-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Google SERP & AEO Scraper",
        "description": "Scrape Google organic SERP with geo targeting, search operators, and flat SEO export. Match Google ranks vs ChatGPT and other LLM citations in one dataset. Includes a Google Sheets template — run scans from a spreadsheet, no code required.",
        "version": "0.0",
        "x-build-id": "TU9xTONwSse1mdOrm"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/morph_coder~google-serp-aeo-scraper/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-morph_coder-google-serp-aeo-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/morph_coder~google-serp-aeo-scraper/runs": {
            "post": {
                "operationId": "runs-sync-morph_coder-google-serp-aeo-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/morph_coder~google-serp-aeo-scraper/run-sync": {
            "post": {
                "operationId": "run-sync-morph_coder-google-serp-aeo-scraper",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "queries"
                ],
                "properties": {
                    "queries": {
                        "title": "Search queries",
                        "type": "array",
                        "description": "One or more keywords or Google Search URLs. All results go to one dataset; use the keyword column to filter. API clients may also pass a newline-separated string.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "countryCode": {
                        "title": "Country",
                        "enum": [
                            "us",
                            "gb",
                            "de",
                            "fr",
                            "es",
                            "it",
                            "in",
                            "au",
                            "ca",
                            "br",
                            "mx",
                            "jp",
                            "kr",
                            "nl",
                            "pl",
                            "ua",
                            "ae",
                            "sg",
                            "hk",
                            "tw",
                            "se",
                            "no",
                            "dk",
                            "fi",
                            "be",
                            "at",
                            "ch",
                            "cz",
                            "ro",
                            "pt",
                            "gr",
                            "tr",
                            "il",
                            "sa",
                            "za",
                            "ng",
                            "ph",
                            "id",
                            "my",
                            "th",
                            "vn",
                            "nz",
                            "ie",
                            "ar",
                            "cl",
                            "co",
                            "pe"
                        ],
                        "type": "string",
                        "description": "ISO 3166-1 alpha-2 country code: selects Google domain (google.co.in, …), GOOGLE_SERP proxy country (with fallback), and default UI language.",
                        "default": "us"
                    },
                    "maxPagesPerQuery": {
                        "title": "Max pages per query",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Stop after this many SERP pages when maxResultsPerQuery is not set (1–10). Ignored when maxResultsPerQuery is set — the actor fetches extra pages until the organic target is met."
                    },
                    "maxResultsPerQuery": {
                        "title": "Max organic results per keyword",
                        "minimum": 1,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Top-N organic rows for each keyword (e.g. 20 = top-20 per keyword). With 10 keywords this can mean up to 200 rows — use maxTotalResults to cap the whole run."
                    },
                    "maxTotalResults": {
                        "title": "Max organic results (whole run)",
                        "minimum": 1,
                        "maximum": 1000,
                        "type": "integer",
                        "description": "Optional hard cap on total organic rows across all keywords. When set, keywords are processed sequentially and the budget is consumed in order — e.g. maxTotalResults 34 with two keywords at top-20 gives 20 + 14 rows. Omit for full maxResultsPerQuery on every keyword."
                    },
                    "outputFormat": {
                        "title": "Output format",
                        "enum": [
                            "page",
                            "flat",
                            "both"
                        ],
                        "type": "string",
                        "description": "page = one row per SERP page; flat = one row per result; both = both formats.",
                        "default": "both"
                    },
                    "mobileResults": {
                        "title": "Mobile results",
                        "type": "boolean",
                        "description": "Scrape mobile SERP layout instead of desktop.",
                        "default": false
                    },
                    "forceExactMatch": {
                        "title": "Force exact match",
                        "type": "boolean",
                        "description": "Wrap each query in double quotes.",
                        "default": false
                    },
                    "site": {
                        "title": "Site filter",
                        "type": "string",
                        "description": "Restrict results to a domain (site: operator)."
                    },
                    "relatedToSite": {
                        "title": "Related to site",
                        "type": "string",
                        "description": "Find pages related to a domain (related: operator). Ignored if site is set."
                    },
                    "wordsInTitle": {
                        "title": "Words in title",
                        "type": "array",
                        "description": "Add intitle: filters.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "wordsInText": {
                        "title": "Words in text",
                        "type": "array",
                        "description": "Add intext: filters.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "wordsInUrl": {
                        "title": "Words in URL",
                        "type": "array",
                        "description": "Add inurl: filters.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "fileTypes": {
                        "title": "File types",
                        "type": "array",
                        "description": "Restrict to file types (pdf, doc, xls, ppt, etc.).",
                        "items": {
                            "type": "string"
                        }
                    },
                    "quickDateRange": {
                        "title": "Quick date range",
                        "enum": [
                            "",
                            "d",
                            "d10",
                            "w2",
                            "m6",
                            "y1"
                        ],
                        "type": "string",
                        "description": "Relative date filter: d (1 day), d10, w2, m6, y1."
                    },
                    "beforeDate": {
                        "title": "Before date",
                        "type": "string",
                        "description": "Absolute date filter (YYYY-MM-DD)."
                    },
                    "afterDate": {
                        "title": "After date",
                        "type": "string",
                        "description": "Absolute date filter (YYYY-MM-DD)."
                    },
                    "searchLanguage": {
                        "title": "Search language (lr)",
                        "type": "string",
                        "description": "Restrict result language (Google lr parameter)."
                    },
                    "languageCode": {
                        "title": "UI language (hl)",
                        "type": "string",
                        "description": "Google interface language."
                    },
                    "locationUule": {
                        "title": "Location UULE",
                        "type": "string",
                        "description": "Encoded geolocation (uule parameter)."
                    },
                    "focusOnPaidAds": {
                        "title": "Focus on paid ads",
                        "type": "boolean",
                        "description": "Enhanced ad extraction with up to 3 retries per page.",
                        "default": false
                    },
                    "includeUnfilteredResults": {
                        "title": "Include unfiltered results",
                        "type": "boolean",
                        "description": "Include lower-quality or duplicate results that Google normally filters out.",
                        "default": false
                    },
                    "saveHtml": {
                        "title": "Save HTML to dataset",
                        "type": "boolean",
                        "description": "Store raw SERP HTML in each dataset row (large payloads).",
                        "default": false
                    },
                    "saveHtmlToKeyValueStore": {
                        "title": "Save HTML to key-value store",
                        "type": "boolean",
                        "description": "Save HTML snapshots to the run key-value store and link via htmlSnapshotUrl.",
                        "default": true
                    },
                    "includeIcons": {
                        "title": "Include favicon icons",
                        "type": "boolean",
                        "description": "Embed base64 favicon images in result objects.",
                        "default": false
                    },
                    "disableGoogleSearchResults": {
                        "title": "Disable Google SERP (LLM only)",
                        "type": "boolean",
                        "description": "Skip Google SERP scraping and run LLM add-ons only.",
                        "default": false
                    },
                    "targetBrand": {
                        "title": "Target brand",
                        "type": "string",
                        "description": "Brand name for visibility comparison in LLM results."
                    },
                    "targetDomains": {
                        "title": "Target domains",
                        "type": "array",
                        "description": "Domains to track in results.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "llmSearchScope": {
                        "title": "LLM search scope",
                        "enum": [
                            "perQuery",
                            "perPage"
                        ],
                        "type": "string",
                        "description": "perQuery = one LLM call per keyword; perPage = call on each SERP page.",
                        "default": "perQuery"
                    },
                    "llmQueryMode": {
                        "title": "LLM query mode",
                        "enum": [
                            "sameAsKeyword",
                            "conversational"
                        ],
                        "type": "string",
                        "description": "sameAsKeyword sends the exact Google keyword to the LLM web search (recommended). conversational expands short keywords into a question.",
                        "default": "sameAsKeyword"
                    },
                    "llmApplyTo": {
                        "title": "Run LLM for",
                        "enum": [
                            "brandLike",
                            "targetOnly",
                            "all"
                        ],
                        "type": "string",
                        "description": "brandLike = short brand-style keywords only (default). targetOnly = keywords matching targetBrand/targetDomains. all = every keyword.",
                        "default": "brandLike"
                    },
                    "aiModeSearch": {
                        "title": "Google AI Mode",
                        "type": "object",
                        "description": "Google AI Mode (Playwright UI — coming soon).",
                        "properties": {
                            "enableAiMode": {
                                "title": "Enable AI Mode",
                                "type": "boolean",
                                "description": "Scrape Google AI Mode UI (requires Playwright — coming soon).",
                                "default": false
                            }
                        }
                    },
                    "chatGptSearch": {
                        "title": "ChatGPT search",
                        "type": "object",
                        "description": "Fetch AI answer via OpenAI Responses API with web search (requires OPENAI_API_KEY secret).",
                        "properties": {
                            "enableChatGpt": {
                                "title": "Enable ChatGPT",
                                "type": "boolean",
                                "description": "Fetch a web-grounded AI answer via OpenAI Responses API + web_search (requires OPENAI_API_KEY secret).",
                                "default": false
                            }
                        }
                    },
                    "copilotSearch": {
                        "title": "Copilot search",
                        "type": "object",
                        "description": "Fetch AI answer via Azure OpenAI (requires AZURE_OPENAI_* secrets).",
                        "properties": {
                            "enableCopilot": {
                                "title": "Enable Copilot",
                                "type": "boolean",
                                "description": "Fetch an AI answer for each keyword via Azure OpenAI.",
                                "default": false
                            }
                        }
                    },
                    "geminiSearch": {
                        "title": "Gemini search",
                        "type": "object",
                        "description": "Fetch AI answer via Gemini API (requires GEMINI_API_KEY secret).",
                        "properties": {
                            "enableGemini": {
                                "title": "Enable Gemini",
                                "type": "boolean",
                                "description": "Fetch an AI answer for each keyword via Google Gemini API.",
                                "default": false
                            }
                        }
                    },
                    "perplexitySearch": {
                        "title": "Perplexity search",
                        "type": "object",
                        "description": "Fetch AI answer via Perplexity API (requires PERPLEXITY_API_KEY secret).",
                        "properties": {
                            "enablePerplexity": {
                                "title": "Enable Perplexity",
                                "type": "boolean",
                                "description": "Fetch an AI answer with citations via Perplexity Sonar API.",
                                "default": false
                            },
                            "returnImages": {
                                "title": "Return images",
                                "type": "boolean",
                                "description": "Include image URLs in the Perplexity response when available.",
                                "default": false
                            },
                            "returnRelatedQuestions": {
                                "title": "Return related questions",
                                "type": "boolean",
                                "description": "Include related follow-up questions from Perplexity.",
                                "default": false
                            },
                            "searchRecency": {
                                "title": "Search recency",
                                "type": "string",
                                "description": "Limit Perplexity web search to recent content.",
                                "enum": [
                                    "day",
                                    "week",
                                    "month",
                                    "year"
                                ],
                                "default": "month"
                            }
                        }
                    },
                    "deepSeekSearch": {
                        "title": "DeepSeek search (experimental)",
                        "type": "object",
                        "description": "Experimental: DeepSeek chat completion only (no web citations). $0.02/call ($20 / 1,000). Requires DEEPSEEK_API_KEY in Actor secrets.",
                        "properties": {
                            "enableDeepSeek": {
                                "title": "Enable DeepSeek (experimental)",
                                "type": "boolean",
                                "description": "Test add-on: chat answer without web search citations. Not suitable for AEO URL ranking yet.",
                                "default": false
                            }
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
