Competitor Analysis API
The calls that measure your brand in AI answers also measure every rival's: whether the assistants recommend them when buyers ask, and which of their pages get cited. The same keywords on Google Search show their organic listings and the ads they buy.
Who it's for
Competitive-intelligence tools, SEO teams running share-of-voice reports, and product marketers tracking how AI engines compare vendors.
What you get
- Which brands AI engines recommend and which domains they cite for category prompts
- The full SERP for each keyword, with the ads competitors pay for
- Scheduled async runs for daily or weekly diffing
Try it
curl -X POST "https://api.answerline.dev/v1/monitor/chatgpt" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"iPhone 17 or Galaxy S26: which should I buy?","country":"US"}'
Engines for this workload
Competitive analysis as a measurement system
The result page that gives your position lists every rival’s position, the ads they paid for, and the features they hold. The AI answer that mentions you also shows who else the engine recommends and whose pages it cites. Competitor analysis collects that data across a fixed competitor set and a fixed keyword set, so every gain or loss can be attributed to someone.
Google Search supplies the organic, ad and feature data below. The AI side runs on ChatGPT, Gemini and AI Mode, in 213 countries, and Copilot, Perplexity and Grok, in any country.
Metrics
| Metric | Definition |
|---|---|
| Keyword overlap | Keywords where both you and competitor C rank within the tracked depth ÷ keywords where either ranks. |
| Keyword gap | Keywords where C ranks in the top 10 and you do not. |
| SERP share of voice | For domain D: Σ over keywords of weight(rank of D) ÷ Σ over keywords of Σ over all results of weight(rank). Weights are a click curve you choose. |
| Ad presence rate | Samples of a keyword’s result page where D has at least one ad ÷ samples taken. |
| Top-of-page ad share | D’s ads with blockPosition top ÷ all top ads observed, across keywords and samples. |
| Ad copy churn | Distinct (title, description) pairs seen for D in a period. High churn suggests active testing. |
| Feature ownership | Keywords where D holds a given feature: an AI Overview source, a People Also Ask link, a shopping card store, a knowledge graph panel. |
| AI citation share | D’s entries in result.sources ÷ all sources, over a prompt set on one engine. |
| AI mention share | Answers whose text names brand B ÷ answers naming any tracked brand. |
| Recommendation order | Rank of B among tracked brands by first appearance in the answer text, averaged over answers that mention B. |
Share of voice needs the same denominator for every domain. Compute it over all results on the page, including domains you don’t track, so a new entrant shows up as your share dropping.
Architecture
keyword set ─┐ ┌─> serp_results, ads
├─> scheduler ─> batches ─> webhooks ─┤
prompt set ──┘ └─> ai_sources, ai_mentions
│
competitor registry (domains, brand names) ─────────> share-of-voice views, diffs
The submit, receive and reconcile loop is the one in the AI visibility tracking guide; delivery semantics are in async tasks and webhooks.
Competitor registry
Keep every domain and brand alias each competitor uses: main site, blog subdomain, docs, regional ccTLDs, product names. Label every other domain you see “untracked” but keep it; the top untracked domains by share of voice are the competitors you didn’t know you had.
Keyword and prompt design
- Category keywords (“project management software”), where share of voice is decided.
- Comparison keywords (“asana vs monday”, “alternatives to trello”), where competitors intercept each other’s brand demand and frequently buy ads.
- Competitor brand keywords (“monday pricing”), which show who bids on whose brand.
- AI prompts that mirror category and comparison intent in natural language: “Which project management tool is best for a 30-person agency?”, “Is Asana or Monday better for client work?”.
Tag each keyword with its group. A comparison keyword usually belongs to two competitors; store both.
Schedule
- Organic results: daily or weekly, one sample.
- Ads: several samples a day on keywords where paid competition matters, because the advertisers shown can differ between searches. Put a sample index in the idempotency key.
- AI answers: weekly, with repeat samples for the most commercially important prompts, because answers vary between runs.
Keys:
serp:{keyword_id}:{country}:{run_date}:{sample}
ai:{engine}:{prompt_id}:{country}:{run_date}:{sample}
Storage schema
create table competitors (
competitor text not null,
domain text,
alias text,
check (domain is not null or alias is not null)
);
create table observations (
task_id uuid primary key,
kind text not null, -- 'serp' or 'ai'
engine text not null,
subject_id text not null, -- keyword_id or prompt_id
country text not null,
run_date date not null,
sample int not null,
observed_at timestamptz,
status text not null default 'QUEUED'
);
create table serp_results (
task_id uuid references observations,
rank int not null,
domain text not null,
link text not null,
primary key (task_id, rank)
);
create table ads (
task_id uuid references observations,
ordinal int not null, -- index in result.ads
block_position text,
position int,
type text, -- RESULT or SHOPPING_CARD
domain text,
title text,
description text,
store text,
copy_hash text not null,
primary key (task_id, ordinal)
);
create table ai_sources (
task_id uuid references observations,
position int,
domain text not null,
url text not null,
primary key (task_id, url)
);
create table ai_mentions (
task_id uuid references observations,
competitor text not null,
first_offset int not null,
primary key (task_id, competitor)
);
Change detection
Diff each run against the previous run of the same subject and market:
- A competitor enters or leaves the top 10 for a keyword.
- A new domain appears in the top 10 of several category keywords at once.
- A competitor starts bidding on your brand keyword (its first ad observation there).
- New ad copy: a
copy_hashnot seen for that domain in the last 30 days. - A competitor gains or loses an AI citation on a prompt for two consecutive runs.
End-to-end Python
Build the tasks
import datetime as dt
HOOK = {"url": "https://hooks.example.com/competitive"}
def serp_task(kw_id, query, country, day, sample):
return {"taskType": "GOOGLE", "payload": {"query": query, "country": country},
"idempotencyKey": f"serp:{kw_id}:{country}:{day}:{sample}", "webhook": HOOK}
def ai_task(engine, prompt_id, prompt, country, day, sample):
return {"taskType": engine, "payload": {"prompt": prompt, "country": country},
"idempotencyKey": f"ai:{engine}:{prompt_id}:{country}:{day}:{sample}", "webhook": HOOK}
today = dt.date.today().isoformat()
tasks = [serp_task(k["id"], k["text"], "US", today, s) for k in keywords for s in range(k["samples"])]
tasks += [ai_task(e, p["id"], p["text"], "US", today, 0) for p in prompts for e in ("CHATGPT", "GEMINI")]
Submit in chunks of 500 and insert an observations row per accepted task. A serp: key has no engine segment, so record GOOGLE for those.
Parse a delivery
import hashlib, re
from urllib.parse import urlsplit
def host(url):
return (urlsplit(url or "").hostname or "").removeprefix("www.")
def apply(db, d):
task = d["task"]
with db.transaction():
row = db.execute(
"update observations set status = %s, observed_at = now() "
"where task_id = %s and status not in ('COMPLETED','FAILED') returning kind",
(task["status"], task["id"])).fetchone()
if not row or task["status"] != "COMPLETED":
return
r = d["response"]["result"]
if row[0] == "serp":
organic = sorted(r.get("organicResults", []), key=lambda o: (o.get("page", 1), o.get("position", 0)))
for rank, o in enumerate(organic, 1):
db.execute("insert into serp_results values (%s,%s,%s,%s)", (task["id"], rank, host(o["link"]), o["link"]))
for n, a in enumerate(r.get("ads", [])):
domain = host(a.get("url")) if a.get("type") == "RESULT" else None
copy = hashlib.sha1(f"{a.get('title','')}\n{a.get('description','')}".encode()).hexdigest()
db.execute("insert into ads values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) on conflict do nothing",
(task["id"], n, a.get("blockPosition"), a.get("position"), a.get("type"), domain,
a.get("title"), a.get("description"), a.get("store"), copy))
else:
for s in r.get("sources", []):
db.execute("insert into ai_sources values (%s,%s,%s,%s) on conflict do nothing",
(task["id"], s.get("position"), host(s["url"]), s["url"]))
text = r.get("text", "")
for competitor, alias in db.execute("select competitor, alias from competitors where alias is not null"):
if m := re.search(rf"\b{re.escape(alias)}\b", text, re.I):
db.execute("insert into ai_mentions values (%s,%s,%s) on conflict (task_id, competitor) "
"do update set first_offset = least(ai_mentions.first_offset, excluded.first_offset)",
(task["id"], competitor, m.start()))
The status guard is the deduplication: a repeated delivery finds the row already final and parses nothing.
Text ads (type RESULT) are grouped by the host of their destination url; domain and displayedUrl are also returned if you prefer the advertiser as displayed. For shopping-style cards (type SHOPPING_CARD) the url is a Google aclk? redirect, so group those by store.
Compute share of voice and AI citation share
-- Click-weighted SERP share of voice per competitor for one day (ctr table holds your weights)
with weighted as (
select coalesce(c.competitor, 'untracked:' || r.domain) as owner, w.weight
from serp_results r
join observations o using (task_id)
join ctr w on w.rank = r.rank
left join competitors c on c.domain = r.domain
where o.kind = 'serp' and o.sample = 0 and o.run_date = '2026-09-14'
)
select owner, sum(weight) / sum(sum(weight)) over () as share_of_voice
from weighted group by owner order by share_of_voice desc limit 25;
-- Ad presence rate per advertiser per keyword over a week of samples
select o.subject_id as keyword_id, coalesce(a.domain, 'store:' || a.store) as advertiser,
count(distinct o.task_id)::numeric
/ (select count(*) from observations o2
where o2.kind = 'serp' and o2.subject_id = o.subject_id and o2.status = 'COMPLETED'
and o2.run_date between '2026-09-08' and '2026-09-14') as ad_presence_rate
from ads a join observations o using (task_id)
where o.run_date between '2026-09-08' and '2026-09-14'
group by o.subject_id, advertiser;
-- AI citation share per competitor per engine
select o.engine, coalesce(c.competitor, 'untracked') as owner,
count(*)::numeric / sum(count(*)) over (partition by o.engine) as citation_share
from ai_sources s join observations o using (task_id)
left join competitors c on c.domain = s.domain
where o.run_date = '2026-09-14'
group by o.engine, owner;
Organic share of voice filters on sample = 0 so ad sampling does not multiply organic counts.
Cost planning
Price per request at the Starter tier: Google Search $0.00103 for one page, plus $0.00071 per extra page and $0.00071 for the AI Overview; ChatGPT $0.0006; Gemini $0.0006; Copilot $0.00274; AI Mode $0.00206. Higher tiers, reached by 30-day spend, cost less. See balance and pricing.
Worked example: a SaaS company tracks 4 competitors in one market.
| Workload | Volume | Cost |
|---|---|---|
| 400 category and comparison keywords, organic, daily | 400 × $0.00103 × 30 | $12.36 |
| 60 paid-competition keywords, 3 extra ad samples a day | 60 × 3 × $0.00103 × 30 | $5.56 |
| 80 AI prompts on ChatGPT and Gemini, weekly | 80 × ($0.0006 + $0.0006) × 4 | $0.384 |
| 20 key prompts, 2 extra samples each, both engines | 20 × 2 × $0.0012 × 4 | $0.192 |
| Monthly total | $18.50 |
Adding the AI Overview to the daily organic run adds 400 × $0.00071 × 30 = $8.52. Running organic weekly instead drops the first row to $1.65. Adding Copilot and AI Mode to the weekly prompts adds 80 × ($0.00274 + $0.00206) × 4 = $1.54. Tiers are on pricing.
Pitfalls
Ignoring untracked domains. Share of voice computed only across named competitors always sums to 100% of a shrinking pie. Include every domain.
One ad sample per day. A single sample can miss an advertiser that appears in most auctions, or catch one that rarely appears. Sample repeatedly and report presence rates, not yes/no.
Grouping shopping ads by URL. Sponsored shopping cards carry a Google redirect as url. Group by store.
Subdomains and ccTLDs. A competitor’s blog, docs and country sites belong to the competitor. Map them in the registry, or share of voice under-counts large competitors.
Brand names that are common words. Match aliases with word boundaries, prefer multi-word aliases, and review a sample of matches whenever you add an alias.
Comparing AI engines on raw counts. Engines return different numbers of sources per answer. Compare citation share within an engine, not source counts across engines.
Counting ChatGPT answers without search. ChatGPT decides on its own whether to search; disableWebSearch changes nothing today. An answer with no sources showed no search, so compute citation share over answers with sources and report the share without.
Which engines fit
| Engine | Competitive signal |
|---|---|
| Google Search | Organic positions for every domain, text and shopping ads with blockPosition, shopping cards by store, AI Overview sources |
| ChatGPT | Which brands the answer recommends, in what order, and which domains it cites; its ads list comes back empty on the answers served today |
| Gemini | Source lists and inline citationPills per answer for citation share |
| Perplexity | sources and inline citationPills per answer for citation share |
| Copilot | Microsoft’s Bing-grounded answer with its sources and citationPills; shopping cards are coming |
| Grok | The web and X searches behind each answer in searchQueries |
| AI Mode | Google’s conversational answer with its own sources and shopping cards |
Competitor AI answer monitoring and tracking ads in AI answers go deeper on the AI side. The quickstart sends a first request.
Questions
What is SERP share of voice?
The share of click-weighted visibility a domain holds across a keyword set: for each keyword, weight each organic result by a click-through value for its rank, sum the weights per domain, and divide by the total weight across all domains.
Can I see which competitors buy ads on my keywords?
Yes. Google Search responses include result.ads with the advertiser domain, title, description, displayedUrl and blockPosition (top, bottom, middle or rhs) for text ads, and store and price for shopping-style sponsored cards.
How do I measure which competitors AI engines cite?
Run category prompts on ChatGPT, Gemini, Perplexity, Copilot, Grok or AI Mode and group result.sources by domain. Citation share per domain is its source count divided by all sources across the answers; mention share uses brand names found in result.text.
How often should competitor data be refreshed?
Organic positions and AI answers are usually tracked daily or weekly. The advertisers shown can differ from one search to the next, so sample ad presence several times a day on the keywords where paid competition matters.
Does the API return search volume or traffic estimates?
No. It returns what the result page or AI answer contained at the time of the request. Combine it with your own volume or click data if you need traffic estimates.
More use cases
AI Visibility API: brand mentions and citations in AI
The GEO measurement layer: how ChatGPT, Gemini and Copilot describe a brand, with mentions, citations and the wording users see.
Rank Tracking API
Google positions, SERP features and the AI Overview for any keyword, as structured JSON from one call.
Local Rank Tracking API
AI answers and Google results per market as parsed JSON: city-level on AI Mode today, US-state targeting coming soon.