# 📣 SERP Share of Voice - Keyword Visibility Tracker (`that_red_bird/serp-share-of-voice`) Actor

📣 Track your and competitors' position-weighted share of search visibility across a keyword set, over time. Works from SERP results you supply — the reliable path, since Google returns a JS shell to server-side requests. Optional Bing/DuckDuckGo live fetch.

- **URL**: https://apify.com/that\_red\_bird/serp-share-of-voice.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## SERP Share of Voice

### What this actor can and cannot fetch — read this first

**Google cannot be scraped server-side.** When a plain HTTP request (no real browser, no JS
execution) hits Google Search, Google returns a JavaScript shell — no organic results, no AI
Overview, nothing usable. Any actor claiming to scrape Google's HTML directly is either running
a full browser at real cost and real fragility, or lying about what it returns. This actor does
neither.

**Primary path (recommended, always works): bring your own SERP results.** Pass an array of
`{"keyword": "...", "rank": 1, "url": "https://...", "title": "..."}` rows — from whatever SERP
source or Apify actor you already use (Google, Bing, a rank tracker, a manual export, anything).
This actor is then a pure **analytics engine**: it maps results to brands, applies
position-weighting, computes share of voice, diffs against history, and reports movement. Nothing
here depends on a scraper staying alive, so nothing here breaks when Google changes its markup.

**Secondary path (optional, opt-in): live Bing / DuckDuckGo fetch.** Both serve parseable HTML to
plain server-side requests (unlike Google), so `fetchMode: "bing"`, `"duckduckgo"`, or
`"bing+duckduckgo"` will fetch and parse live results for you. This is provided for convenience,
guarded so a parsing failure is recorded in `fetchErrors` and skipped rather than crashing the
run — but their markup can and does change, so treat it as best-effort, not a guarantee.

### What it computes

- **Position-weighted share of voice** — a documented CTR-style decay curve (rank 1 worth
  roughly 10-15x rank 10 by default; override with `ctrCurve`) instead of flat "count of
  rankings", which is what makes most SoV tools useless: being #1 for one keyword should count
  for far more than being #10 for the same keyword.
- **Optional keyword-volume weighting** — supply `keywordVolumes` and a high-volume keyword's
  rankings count proportionally more than a rarely-searched one's.
- **Brand mapping with subdomain handling** — `brandMap: {"Brand": ["domain.com"]}` automatically
  matches `shop.domain.com`, `blog.domain.com`, etc. to that brand; list a subdomain explicitly
  only when it belongs to a *different* brand than its parent.
- **Per-brand SoV%, per-keyword breakdown, and head-to-head gaps** against `yourBrand`.
- **State across runs** in a named key-value store (`stateStoreName`): rolling SoV history per
  brand, movement (delta + trend) since the last run, biggest keyword-level gains/losses, and
  new entrants/dropouts. First run is always a baseline — no movement, no false "everything is
  new" noise.

### Output rows

`type: "brand"` (one per brand, incl. dropouts), `type: "keyword"` (per-keyword breakdown),
`type: "head-to-head"` (you vs. each competitor, worst gap first), `type: "mover"` (biggest
keyword-level gains/losses). `SUMMARY` in the key-value store carries the leader, your SoV,
winners, losers, new entrants and dropouts.

### Typical uses

Weekly/monthly SoV tracking for a keyword set you already rank-track elsewhere · competitive
intelligence — see which competitor is quietly gaining on a specific keyword before it shows up
anywhere else · reporting a defensible, position-weighted number to stakeholders instead of a
flat "we rank for N keywords" count.

### Schedule it

Weekly or monthly, matching whatever cadence you already pull SERP data at. Every run is diffed
against the previous one automatically — there is nothing else to configure for the history to
build up correctly.

# Actor input Schema

## `results` (type: `array`):

The primary input path. An array of {"keyword": "...", "rank": 1, "url": "https://...", "title": "..."} rows from ANY SERP source or actor you already use. Google is deliberately not scraped here (it returns a JS shell to server-side requests) — bring your own results and this actor does the analytics.

## `brandMap` (type: `object`):

Maps each result URL to a brand. Format: {"Your Brand": \["yourdomain.com"], "Competitor A": \["competitor-a.com", "blog.competitor-a.com"]}. Subdomains are matched to the domain you list (e.g. shop.yourdomain.com matches yourdomain.com automatically) — you only need to list a subdomain explicitly if it belongs to a DIFFERENT brand than its parent domain.

## `yourBrand` (type: `string`):

The exact brand name (a key in brandMap) that head-to-head gaps and the summary's "yourSoV" are computed against. Leave blank to skip head-to-head rows.

## `ctrCurve` (type: `array`):

Optional override for the default position-weighting (CTR-style decay) curve. Array of integers in basis points (10000 = 100%), index 0 = rank 1. Apify input schemas have no float type, so this is expressed in basis points rather than a 0-1 fraction. Leave empty to use the documented default curve (rank 1 worth roughly 10-15x rank 10).

## `keywordVolumes` (type: `object`):

Optional. {"keyword": 1000, ...} monthly search volume per keyword. When supplied, each result's position weight is multiplied by its keyword's volume, so visibility on a high-volume term counts for more than the same rank on a rarely-searched one. Omit to weight every keyword equally.

## `fetchMode` (type: `string`):

Secondary, optional path. "supplied" (default) uses only the results you pass in above — the reliable option. "bing"/"duckduckgo"/"bing+duckduckgo" additionally fetch live results server-side for the keywords you list below. Google is NOT an option: it serves a JavaScript shell (no organic results, no AI Overview) to server-side requests and cannot be scraped this way.

## `keywords` (type: `array`):

Only used when fetchMode is not "supplied". The keyword list to fetch live results for from Bing and/or DuckDuckGo.

## `maxResultsPerKeyword` (type: `integer`):

Upper bound on organic results parsed per keyword when fetchMode is not "supplied".

## `country` (type: `string`):

Optional market hint passed to Bing (e.g. "US", "GB") when fetchMode includes bing. Ignored otherwise.

## `concurrency` (type: `integer`):

How many keyword/source fetches to run in parallel when fetchMode is not "supplied".

## `timeoutMs` (type: `integer`):

Per-request timeout in milliseconds for live Bing/DuckDuckGo fetches.

## `onlyChanges` (type: `boolean`):

When true (and this isn't the first run), brand rows whose share-of-voice trend is "flat" are omitted from the dataset. Keyword, head-to-head and mover rows are always included.

## `maxItems` (type: `integer`):

Hard cap on rows pushed to the dataset this run. 0 = no cap.

## `resetBaseline` (type: `boolean`):

Ignore any previously stored share-of-voice state and treat this run as a fresh first run (no movement, no dropout/new-entrant detection).

## `keepHistory` (type: `integer`):

How many past runs to keep in each brand's rolling share-of-voice history, so you can chart it over time without your own database.

## `moversLimit` (type: `integer`):

How many keyword-level biggest-gain and biggest-loss rows to emit per run.

## `stateStoreName` (type: `string`):

Name of the named key-value store used to persist share-of-voice history between runs. Use a different name to run independent, non-interfering trackers (e.g. per client or per keyword set).

## `proxyConfiguration` (type: `object`):

Optional Apify proxy configuration, used only for live Bing/DuckDuckGo fetches when fetchMode is not "supplied".

## Actor input object example

```json
{
  "results": [
    {
      "keyword": "best running shoes",
      "rank": 1,
      "url": "https://nike.com/running-shoes",
      "title": "Best Running Shoes | Nike"
    },
    {
      "keyword": "best running shoes",
      "rank": 2,
      "url": "https://adidas.com/running",
      "title": "Running Shoes | adidas"
    },
    {
      "keyword": "best running shoes",
      "rank": 3,
      "url": "https://runnersworld.com/gear/best-running-shoes",
      "title": "The Best Running Shoes, Tested - Runner's World"
    },
    {
      "keyword": "marathon training plan",
      "rank": 1,
      "url": "https://adidas.com/training/marathon",
      "title": "Marathon Training Plan | adidas"
    },
    {
      "keyword": "marathon training plan",
      "rank": 2,
      "url": "https://runnersworld.com/training/marathon-plan",
      "title": "Marathon Training Plan - Runner's World"
    },
    {
      "keyword": "marathon training plan",
      "rank": 3,
      "url": "https://nike.com/marathon",
      "title": "Marathon Guide | Nike"
    }
  ],
  "brandMap": {
    "Nike": [
      "nike.com"
    ],
    "adidas": [
      "adidas.com"
    ],
    "Runner's World": [
      "runnersworld.com"
    ]
  },
  "yourBrand": "Nike",
  "ctrCurve": [],
  "keywordVolumes": {},
  "fetchMode": "supplied",
  "keywords": [],
  "maxResultsPerKeyword": 10,
  "country": "",
  "concurrency": 3,
  "timeoutMs": 15000,
  "onlyChanges": false,
  "maxItems": 0,
  "resetBaseline": false,
  "keepHistory": 60,
  "moversLimit": 10,
  "stateStoreName": "serp-share-of-voice-state"
}
```

# Actor output Schema

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

No description

## `downloadCsv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `count` (type: `string`):

No description

# 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 = {
    "results": [
        {
            "keyword": "best running shoes",
            "rank": 1,
            "url": "https://nike.com/running-shoes",
            "title": "Best Running Shoes | Nike"
        },
        {
            "keyword": "best running shoes",
            "rank": 2,
            "url": "https://adidas.com/running",
            "title": "Running Shoes | adidas"
        },
        {
            "keyword": "best running shoes",
            "rank": 3,
            "url": "https://runnersworld.com/gear/best-running-shoes",
            "title": "The Best Running Shoes, Tested - Runner's World"
        },
        {
            "keyword": "marathon training plan",
            "rank": 1,
            "url": "https://adidas.com/training/marathon",
            "title": "Marathon Training Plan | adidas"
        },
        {
            "keyword": "marathon training plan",
            "rank": 2,
            "url": "https://runnersworld.com/training/marathon-plan",
            "title": "Marathon Training Plan - Runner's World"
        },
        {
            "keyword": "marathon training plan",
            "rank": 3,
            "url": "https://nike.com/marathon",
            "title": "Marathon Guide | Nike"
        }
    ],
    "brandMap": {
        "Nike": [
            "nike.com"
        ],
        "adidas": [
            "adidas.com"
        ],
        "Runner's World": [
            "runnersworld.com"
        ]
    },
    "yourBrand": "Nike"
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/serp-share-of-voice").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 = {
    "results": [
        {
            "keyword": "best running shoes",
            "rank": 1,
            "url": "https://nike.com/running-shoes",
            "title": "Best Running Shoes | Nike",
        },
        {
            "keyword": "best running shoes",
            "rank": 2,
            "url": "https://adidas.com/running",
            "title": "Running Shoes | adidas",
        },
        {
            "keyword": "best running shoes",
            "rank": 3,
            "url": "https://runnersworld.com/gear/best-running-shoes",
            "title": "The Best Running Shoes, Tested - Runner's World",
        },
        {
            "keyword": "marathon training plan",
            "rank": 1,
            "url": "https://adidas.com/training/marathon",
            "title": "Marathon Training Plan | adidas",
        },
        {
            "keyword": "marathon training plan",
            "rank": 2,
            "url": "https://runnersworld.com/training/marathon-plan",
            "title": "Marathon Training Plan - Runner's World",
        },
        {
            "keyword": "marathon training plan",
            "rank": 3,
            "url": "https://nike.com/marathon",
            "title": "Marathon Guide | Nike",
        },
    ],
    "brandMap": {
        "Nike": ["nike.com"],
        "adidas": ["adidas.com"],
        "Runner's World": ["runnersworld.com"],
    },
    "yourBrand": "Nike",
}

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/serp-share-of-voice").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "results": [
    {
      "keyword": "best running shoes",
      "rank": 1,
      "url": "https://nike.com/running-shoes",
      "title": "Best Running Shoes | Nike"
    },
    {
      "keyword": "best running shoes",
      "rank": 2,
      "url": "https://adidas.com/running",
      "title": "Running Shoes | adidas"
    },
    {
      "keyword": "best running shoes",
      "rank": 3,
      "url": "https://runnersworld.com/gear/best-running-shoes",
      "title": "The Best Running Shoes, Tested - Runner'\''s World"
    },
    {
      "keyword": "marathon training plan",
      "rank": 1,
      "url": "https://adidas.com/training/marathon",
      "title": "Marathon Training Plan | adidas"
    },
    {
      "keyword": "marathon training plan",
      "rank": 2,
      "url": "https://runnersworld.com/training/marathon-plan",
      "title": "Marathon Training Plan - Runner'\''s World"
    },
    {
      "keyword": "marathon training plan",
      "rank": 3,
      "url": "https://nike.com/marathon",
      "title": "Marathon Guide | Nike"
    }
  ],
  "brandMap": {
    "Nike": [
      "nike.com"
    ],
    "adidas": [
      "adidas.com"
    ],
    "Runner'\''s World": [
      "runnersworld.com"
    ]
  },
  "yourBrand": "Nike"
}' |
apify call that_red_bird/serp-share-of-voice --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,that_red_bird/serp-share-of-voice"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9jfrL9ISg1IimVNcC/builds/gsQInFQ9hH4J6emOJ/openapi.json
