cloro
Analytics

Competitor SEO Tracking: Monitor Any Competitor's Rankings in Real Time

Ricardo Batista
Ricardo Batista
Founder, cloro
8 min read
Competitor AnalysisShare of VoiceRank Tracking
On this page

Competitor SEO tracking is not “watching your own rankings.” It is watching the whole board: which domains own the top 10 for the keywords you care about, how that ownership shifts week to week, and who just muscled in or fell off. Your own position is one column in that table. The interesting signal is everyone else’s.

The metric that makes competitor SEO tracking tractable is share of voice (SoV). It is the percentage of top-10 organic slots, across your entire tracked keyword set, that each domain holds. One number collapses hundreds of individual rankings into a single leaderboard. You can trend it, alert on it, and put it in front of a stakeholder.

One clarification up front, because the term “share of voice” now means two different things. This post is about classic Google SERP share of voice — blue-link organic results. The AI-era version asks whether ChatGPT, Perplexity, or Google’s AI Overview name and cite your brand in their synthesized answers. That is a different measurement with different mechanics, and we cover it in AI share of voice.

The two move independently. A domain can own the blue links and be invisible in AI answers, or get cited constantly and rank nowhere. Track both; don’t conflate them.

The head-term use case — building a full competitive picture across organic, ads, AI Overview, and PAA — lives on the competitor analysis page. This post takes the competitor SEO tracking slice. We stand up a SERP share-of-voice monitor, run it for 30 days in one niche to see what the data actually looks like, and wire alerts. If you want a buyer’s-guide comparison of off-the-shelf tools instead of building it yourself, see the best competitive intelligence tools.

Build a SERP share-of-voice monitor

The four signals worth watching in competitor SEO tracking: share of voice (your percentage of the tracked SERPs), ranking entries and exits (who gained or lost pages), single-day swings (sudden position moves), and alert thresholds (acting only on real moves)

The monitor has three moving parts:

  1. A keyword set — the queries that define your competitive space.
  2. A scheduled SERP fetch — pull the organic results for every keyword, every day.
  3. An aggregation — roll the organic results up by domain into a share-of-voice leaderboard.

The only part that needs infrastructure is the SERP fetch. Scraping Google directly means proxies, CAPTCHAs, and parser maintenance. A SERP API returns structured results instead. That lets you spend your time on the aggregation, not the plumbing — which is where every competitor SEO tracking project actually earns its keep.

Fetch one SERP

Start with a single query. cloro’s Google Search endpoint returns organic results as structured JSON:

curl -s -X POST "https://api.cloro.dev/v1/monitor/google" \
  -H "Authorization: Bearer sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "best crm software",
    "country": "US",
    "pages": 1
  }'

The relevant slice of the response — the top of the organic block — looks like this:

{
  "success": true,
  "result": {
    "organicResults": [
      {
        "position": 1,
        "title": "Best CRM Software of 2026",
        "link": "https://www.salesforce.com/crm/best-crm/",
        "displayedLink": "salesforce.com",
        "page": 1
      },
      {
        "position": 2,
        "title": "16 Best CRM Software Compared",
        "link": "https://www.forbes.com/advisor/business/software/best-crm/",
        "displayedLink": "forbes.com",
        "page": 1
      },
      {
        "position": 3,
        "title": "Best CRM of 2026 - Reviews",
        "link": "https://www.reddit.com/r/sales/comments/best_crm/",
        "displayedLink": "reddit.com",
        "page": 1
      }
    ]
  }
}

Each organic result carries a position and a displayedLink (the registrable domain Google shows). Those two fields are everything the share-of-voice calculation needs.

Aggregate into share of voice

Now run every keyword in the set and roll the organic results up by domain. Two numbers matter, and they answer different questions:

  • Raw top-10 share — of all the top-10 slots across your keyword set, what fraction does each domain hold? Answers: who is present?
  • Position-weighted share — weight each slot by rank (position 1 = 10 points, position 2 = 9, … position 10 = 1), then divide each domain’s points by the total points available. Answers: who is present and ranking high?

The weighted version is the one to trend. A domain that owns three #9 rankings and a domain that owns three #1 rankings have identical raw share but wildly different real estate.

import requests
from collections import defaultdict
from urllib.parse import urlparse

API_KEY = "sk_live_your_api_key"
ENDPOINT = "https://api.cloro.dev/v1/monitor/google"

# Your competitive keyword set — the queries that define the space.
KEYWORDS = [
    "best crm software",
    "crm software",
    "salesforce alternatives",
    "hubspot vs salesforce",
    # ... 58 more
]

def registrable_domain(url: str) -> str:
    host = urlparse(url).netloc.lower().removeprefix("www.")
    return host

def fetch_top10(query: str) -> list[dict]:
    resp = requests.post(
        ENDPOINT,
        headers={"Authorization": f"Bearer {API_KEY}",
                 "Content-Type": "application/json"},
        json={"query": query, "country": "US", "pages": 1},
        timeout=60,
    )
    resp.raise_for_status()
    results = resp.json()["result"]["organicResults"]
    return [r for r in results if r["position"] <= 10]

raw_slots = defaultdict(int)       # count of top-10 appearances
weighted_points = defaultdict(int) # position-weighted points
total_slots = 0
total_points = 0

for kw in KEYWORDS:
    for r in fetch_top10(kw):
        domain = registrable_domain(r["link"])
        weight = 11 - r["position"]   # pos 1 -> 10, pos 10 -> 1
        raw_slots[domain] += 1
        weighted_points[domain] += weight
        total_slots += 1
        total_points += weight

# Leaderboard, sorted by weighted share of voice.
print(f"{'domain':<28} {'raw SoV':>9} {'weighted SoV':>13}")
for domain in sorted(weighted_points, key=weighted_points.get, reverse=True)[:10]:
    raw = raw_slots[domain] / total_slots * 100
    weighted = weighted_points[domain] / total_points * 100
    print(f"{domain:<28} {raw:>8.1f}% {weighted:>12.1f}%")

Run this on a schedule — a cron job, a Cloudflare Worker, a GitHub Action, anything that can fire once a day. Append each day’s leaderboard to a table keyed by date. That is a working competitor SEO tracking system. Everything after this is analysis.

To make the raw-vs-weighted distinction concrete, take a tiny three-keyword set and two domains. Domain A ranks #1, #2, #1; domain B ranks #8, #9, #10. Both appear three times, so their raw share is identical — 3 slots each. But the weights tell the real story: A earns 10 + 9 + 10 = 29 points; B earns 3 + 2 + 1 = 6 points. On the position-weighted board, A owns nearly five times the real estate:

DomainPositionsRaw slotsWeighted points
A#1, #2, #1329
B#8, #9, #1036

Raw share tells you who shows up. Weighted share tells you who wins. Report the weighted number as your headline and keep raw as a secondary “reach” figure.

Choosing the keyword set

The monitor is only as good as the keywords feeding it. A useful competitive set is a mix of three intents: head commercial terms (crm software), comparison and alternative queries (salesforce alternatives, hubspot vs salesforce), and problem/solution long-tail (crm for small business). Comparison and alternative queries are where competitors and review sites fight hardest. They carry the most competitive signal per slot. Still, weight the set toward your real buyer’s language, not just the highest-volume heads.

Whatever mix you pick, freeze it. Share of voice is only comparable day over day when the denominator — the keyword set — doesn’t change underneath you.

A note on domain grouping: registrable_domain above treats trailhead.salesforce.com and help.salesforce.com as distinct domains from salesforce.com. For a strict “who owns the slot” view that’s correct — those are different pages competing on their own merits. For a “who owns the brand” view you’d collapse subdomains onto the eTLD+1. Both are defensible; just pick one and be consistent, because the choice materially changes the leaderboard, as the data below shows.

The data study: a 30-day share-of-voice war

To see what this monitor actually produces, we ran it. 62 tracked queries in the CRM space, daily, from 2026-06-05 to 2026-07-05 — 1,785 SERPs in total. Every organic top-10 result, rolled up by domain, every day for a month.

Read this as a methodology demonstration, not a CRM market ranking. We picked CRM because it was the densest daily-tracked cluster we had running. The prompt mix also skews heavily toward Salesforce-comparison queries (“salesforce alternatives”, “hubspot vs salesforce”, and the like). That biases the board toward Salesforce and its orbit.

The point here is the shape of the analysis — the leaderboard, the movement, the events. It transfers directly to whatever keyword set defines your space. Run competitor SEO tracking on your niche and the domains will be different. The mechanics will not.

The leaderboard moved

Over the 30 days, salesforce.com climbed from 9.5% to 10.8% of top-10 slots — a steady expansion of its footprint across the tracked set. reddit.com held the user-generated-content lane at a 5–6% band throughout. It was the clearest non-vendor presence on the board. Reddit is the “what do people actually use” answer that Google keeps surfacing for commercial-investigation queries.

DomainTop-10 share (month open)Top-10 share (month close)
salesforce.com9.5%10.8%
reddit.com5–6% band5–6% band

Share of the ~620 daily top-10 slots (62 queries × 10), CRM set, 2026-06-05 → 07-05.

Below the two leaders sat the Salesforce satellite clustertrailhead.salesforce.com (Salesforce’s own learning platform), salesforceben.com (the largest independent Salesforce community/review site), and help.salesforce.com (product documentation). These three traded the mid-slots among themselves across the month, no single one holding steady. This is why the subdomain-grouping decision matters. Collapse them onto salesforce.com and Salesforce’s brand-level share jumps well past 10.8%. The satellites are, in aggregate, a second Salesforce presence on the same board.

The takeaway that generalizes: the leaderboard is not just competitors. It’s vendors, their own satellite properties, independent review sites, and UGC (Reddit) all competing for the same ten slots. A tracker that only watches named competitors misses the review sites and forums that often own the highest-intent queries.

Entry and exit log

Share-of-voice percentages trend smoothly; the interesting events are discrete. A domain crossing into or out of the top 10 is a step change a trend line can smear over. Watching the weekly top-10 set membership surfaces them:

WeekEventDomain
Week 24 (Jun 09–15)Entered weekly top-10a Salesforce partner/consultancy domain
Week 24 (Jun 09–15)Dropped out of weekly top-10translate.google.com

The week-24 swap is a textbook example of why you log membership, not just share. A partner/consultancy domain breaking into the weekly top 10 is a competitive signal — someone’s content or link-building landed.

The translate.google.com exit is the other kind of signal. It’s not a competitor at all. It’s a Google property that had been catching a slot on some queries (translated result pages) and then stopped. Your tracker sees both as “top-10 membership changed.” Your judgment separates the competitive move from the SERP-feature noise.

Biggest single-day swing

The largest one-day share move in the entire window belonged to reddit.com: +2.8 points from June 5 to June 6. A jump that size in 24 hours is almost never organic ranking earned overnight. It is Google dialing UGC exposure up across a swath of the query set at once. Call it a “Reddit boost” ranking adjustment — a common pattern for commercial-investigation queries in this period. One domain, one day, +2.8 points of the board.

This is the value of daily granularity. A weekly or monthly rollup would have averaged that spike into a gentle slope. You’d never know a platform-level shift happened on a specific date. A dated event is exactly the kind of thing you correlate against a known Google update or a competitor’s launch.

Alerting: catch entries and exits in real time

A leaderboard you check manually is a report. A leaderboard that pings you when the board changes is a monitor. This is where competitor SEO tracking earns its “real time” label. The events worth interrupting someone for are exactly the entry/exit transitions from the study: a new domain in the top 10, or an incumbent falling out.

The recipe: store today’s top-10 domain set per keyword, diff it against yesterday’s stored set, and fire a webhook on any difference.

import json
import os
import requests
from urllib.parse import urlparse

SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK_URL"]
STATE_FILE = "top10_state.json"   # { "keyword": ["domain", ...], ... }

def registrable_domain(url: str) -> str:
    return urlparse(url).netloc.lower().removeprefix("www.")

def current_top10(query: str) -> set[str]:
    resp = requests.post(
        "https://api.cloro.dev/v1/monitor/google",
        headers={"Authorization": f"Bearer {os.environ['CLORO_API_KEY']}",
                 "Content-Type": "application/json"},
        json={"query": query, "country": "US", "pages": 1},
        timeout=60,
    )
    resp.raise_for_status()
    results = resp.json()["result"]["organicResults"]
    return {registrable_domain(r["link"]) for r in results if r["position"] <= 10}

def alert(query: str, entered: set[str], exited: set[str]) -> None:
    lines = [f"*Top-10 change for* `{query}`"]
    for d in sorted(entered):
        lines.append(f"  :new: *entered* — {d}")
    for d in sorted(exited):
        lines.append(f"  :x: *dropped out* — {d}")
    requests.post(SLACK_WEBHOOK, json={"text": "\n".join(lines)}, timeout=30)

def run(keywords: list[str]) -> None:
    previous = json.load(open(STATE_FILE)) if os.path.exists(STATE_FILE) else {}
    updated = {}
    for kw in keywords:
        today = current_top10(kw)
        yesterday = set(previous.get(kw, []))
        entered = today - yesterday
        exited = yesterday - today
        if yesterday and (entered or exited):   # skip first-ever run (no baseline)
            alert(kw, entered, exited)
        updated[kw] = sorted(today)
    json.dump(updated, open(STATE_FILE, "w"), indent=2)

if __name__ == "__main__":
    run(["best crm software", "salesforce alternatives", "hubspot vs salesforce"])

Schedule this right after your daily SERP pull. A few refinements once the basics work:

  • Filter the noise. In the study, translate.google.com dropping out was not a competitive event. Maintain an ignore-list of Google properties and known non-competitors so alerts stay high-signal.
  • Only alert on the domains you care about. For a tighter feed, alert only when a named competitor (or a brand-new domain you’ve never seen) enters — not on every reshuffle among incumbents.
  • Route by severity. A competitor entering your #1 keyword’s top 3 is a Slack @here; a churn on a long-tail query is a weekly digest. Use the position, not just membership, to decide.

You can push the same diff to any endpoint — PagerDuty, a webhook into your data warehouse, an internal Slack channel per keyword cluster. The mechanism is identical; only the payload target changes.

From tracking to action

A competitor SEO tracking system built this way gives you three layers of signal, each more actionable than the last:

  1. The leaderboard — who owns your space right now, weighted by rank. Your baseline.
  2. The trend — who’s gaining and losing share over weeks, like Salesforce’s 9.5% → 10.8% climb. Your strategy input.
  3. The events — who just entered or exited the top 10, and the single-day swings behind the trend. Your alert stream.

Classic rank tracking gives you row one for a single domain: yours. Share-of-voice tracking gives you the whole board.

And because you’re pulling structured SERP data yourself, you’re not locked into a dashboard’s idea of what a competitor is. You can group subdomains your way, weight positions your way, and alert on the domains you consider threats. That ownership is the real payoff of building competitor SEO tracking rather than renting it.

Then run the AI-answer version alongside it. The blue-link board and the AI share of voice board are diverging fast. The domains winning one are increasingly not the domains winning the other. For the full competitive picture — organic, ads, AI Overview citations, and PAA in one pull — see the competitor analysis use case. For the classic side, read how to track SERP rankings end to end.


Track your competitors’ rankings with cloro’s SERP API. Structured Google Search results — organic, ads, AI Overview, and PAA — built for exactly the share-of-voice monitoring above. Start with 500 free credits at cloro.

Frequently asked questions

What is competitor SEO tracking?+

Competitor SEO tracking is the practice of monitoring which domains rank for your target keywords over time, instead of just tracking your own positions. The core metric is share of voice: what percentage of the top-10 organic slots across your tracked keyword set each competitor owns. You compute it by scraping the SERP for every keyword on a schedule and aggregating the organic results by domain.

How is SERP share of voice different from AI share of voice?+

SERP share of voice measures presence in Google's classic blue-link results — which domains occupy the top 10 across your keyword set. AI share of voice measures whether a model like ChatGPT, Perplexity, or Google's AI Overview names or cites your brand in its synthesized answer. They move independently: a domain can dominate the blue links and be invisible in AI answers, or vice versa. Track both.

How do you calculate share of voice from SERP data?+

Raw share of voice is the count of top-10 slots a domain holds divided by the total slots across all tracked keywords. Position-weighted share of voice assigns each slot a weight (position 1 = 10 points down to position 10 = 1 point), sums a domain's weighted points, and divides by the total available points. The weighted version rewards ranking high, not just ranking at all.

How often should I refresh competitor rankings?+

Daily for a commercial keyword set of a few dozen to a few hundred queries. Google's index doesn't move fast enough on most queries to justify hourly checks, and you'll burn credits chasing rendering noise. Daily is enough to catch entries, exits, and multi-point share swings while keeping the SERP volume — and cost — predictable.

Can I get alerted when a competitor enters or leaves the top 10?+

Yes. Store each day's top-10 domain set per keyword, diff today's set against yesterday's, and fire a webhook (Slack, PagerDuty, your own endpoint) when a domain appears or disappears. This catches new entrants and displaced incumbents the moment they cross the fold, which a share-of-voice trend line alone can hide.