AnswerLineStart free

, Keyword Research · SERP API · Google

Find your competitors' keywords from live SERP data

To find a competitor’s keywords from SERP data, you invert the usual keyword-tool workflow. Instead of asking a database “what does billfox.com rank for”, you build a large candidate set around your market, run every candidate through Google, and record where each competitor’s domain appears in organicResults and ads. The output is exact and current for the keywords you checked, in the market and device you chose. It is not exhaustive, and it has no demand numbers attached.

This API returns no search volume and no keyword difficulty, only what the results page showed. Prioritising the gaps you find needs a volume source, covered at the end.

The method in six steps

  1. Seed the candidate set from your own terms, the competitor’s positioning and their brand name.
  2. Expand it with relatedSearches[].query and peopleAlsoAsk[].question.
  3. Run every candidate and store organic and ad placements per domain.
  4. Probe competitor topics with site: queries to find pages and themes you missed.
  5. Read ads to see which candidates they pay for.
  6. Build an overlap and gap matrix, then join volume and prioritise.

Placeholder names: you are ledgerly.com; rivals are billfox.com and invoicepro.io.

Step 1: seeds

Good seeds come from four places:

Seed source Examples Why
Your product terms invoicing software, recurring invoices, late payment reminders Keywords you should own
Competitor positioning Words from their homepage headline, feature page titles, nav labels Keywords they are trying to own
Competitor brand + modifier billfox pricing, billfox alternatives, billfox vs Where rivals intercept each other
Customer language Phrases from sales calls, support tickets, reviews Keywords neither of you targets yet

Aim for 30 to 100 seeds per product area. Normalise (lowercase, collapse whitespace) and give each an ID.

Every Google Search response carries two expansion sources:

One expansion level is usually enough for competitor keyword discovery, because the goal is a wide candidate set to test, not a deep topic map. Filter children for relevance (share at least one content token with a seed, or pass an embedding threshold), or related searches drift into neighbouring categories. For deeper crawls with clustering, see keyword research and People Also Ask keyword research.

Because the seed run already returns organic results and ads, seeds cost nothing extra in step 3: the same response feeds both expansion and competitor matching.

Step 3: run candidates and match domains

Submit all candidates as async tasks. The same code submits seeds and expanded keywords.

import hashlib, os, requests

API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}

def submit(candidates, country="US", run="ckw-2026-09"):
    ids = {}
    for i in range(0, len(candidates), 500):
        chunk = candidates[i:i + 500]
        tasks = [{
            "taskType": "GOOGLE",
            "payload": {"query": kw, "country": country, "pages": 2},
            "idempotencyKey": f"{run}:{country}:{hashlib.sha1(kw.encode()).hexdigest()[:16]}",
            "webhook": {"url": "https://seo.example.com/hooks/ckw"},
        } for kw in chunk]
        res = requests.post(f"{API}/v1/async/task/batch", json=tasks, headers=HEADERS, timeout=60)
        res.raise_for_status()
        for item in res.json()["results"]:
            if item["success"]:
                ids[item["task"]["id"]] = chunk[item["index"]]
    return ids

pages: 2 checks the top 20, which catches competitor pages sitting just off page one, the most actionable gaps. Each extra page adds 2 credits; use pages: 1 if you only care about the top 10.

When a result arrives (by webhook, verified as in verifying webhook signatures), reduce it to per-domain rows:

from urllib.parse import urlsplit

COMPETITORS = {
    "you": {"ledgerly.com"},
    "billfox": {"billfox.com", "help.billfox.com"},
    "invoicepro": {"invoicepro.io"},
}

def host(url):
    return (urlsplit(url or "").hostname or "").removeprefix("www.")

def owner(domain):
    for name, domains in COMPETITORS.items():
        if domain in domains or any(domain.endswith("." + d) for d in domains):
            return name
    return None

def reduce(keyword, result):
    organic = sorted(result.get("organicResults", []), key=lambda o: (o.get("page", 1), o.get("position", 0)))
    best = {}
    for rank, o in enumerate(organic, 1):
        name = owner(host(o["link"]))
        if name and name not in best:
            best[name] = {"rank": rank, "url": o["link"]}
    advertisers = {owner(a.get("domain") or host(a.get("url"))) for a in result.get("ads", [])
                   if a.get("type") == "RESULT"} - {None}
    children = [s["query"] for s in result.get("relatedSearches", [])]
    children += [q["question"] for q in result.get("peopleAlsoAsk", [])]
    return {"keyword": keyword, "best": best, "advertisers": sorted(advertisers), "children": children}

best holds each tracked competitor’s best organic rank and ranking URL for the keyword. Store every other domain too if you want to discover competitors you did not list; the most frequent unknown domains across the candidate set are usually worth adding.

Step 4: site: queries to probe a competitor’s topics

Google interprets operators in the query text, and Google’s own help page documents site: for restricting results to a site, quotes for exact matches and - to exclude a word (checked 2026-09-17). That makes a query like this useful:

curl -X POST https://api.answerline.dev/v1/monitor/google \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "query": "site:billfox.com recurring invoices", "country": "US" }'

Read organicResults[].title and organicResults[].link. Titles of a competitor’s pages on a topic are a direct source of keyword candidates: feature names, integrations, template pages, glossary terms, comparison pages you did not know existed. Useful probes:

Probe What it surfaces
site:billfox.com vs Their comparison pages and the rivals they target
site:billfox.com template Template and free-tool pages, often built for search
site:billfox.com integration Integration landing pages
site:billfox.com "late fees" Pages using an exact phrase, via quotes

Treat these as a sample. A site: search returns what Google chooses to show for that query, not an export of the competitor’s indexed pages, and it says nothing about which keywords those pages rank for. Feed the titles back into step 3 as candidates, where rank is measured on the real query.

Step 5: ads show paid keywords

For every candidate you already have ads[]. Text ads (type RESULT) carry domain, blockPosition, title and description. A competitor’s domain appearing there means their campaigns showed an ad for that search in that market at that moment; whether they bid on the exact keyword or a broader match is not visible.

The advertisers on a page can differ between searches, so one sample under-counts. For the candidates where paid competition matters (brand terms, high-intent category terms), run two or three extra samples on different days and compute:

ad presence rate = samples with their ad ÷ samples taken

Keywords with a high ad presence rate and no organic ranking for that competitor are keywords they pay for because they cannot rank. Those are often good organic targets for you: commercial intent is confirmed by someone’s budget, and the organic slot is not theirs.

Step 6: overlap and gap matrix

With best and advertisers per keyword, build one row per keyword:

Keyword You BillFox InvoicePro BillFox ads Class
recurring invoice software 4 2 9 yes Shared, behind
late payment reminder email 3 no Gap
invoice template for contractors 1 12 no Strength
billfox alternatives 7 1 5 no Shared, behind
multi-currency invoicing yes Paid only

(Illustrative rows.) Classes:

Class Rule Action
Strength You rank ≤ 10, competitor absent or below you Defend and extend
Shared, ahead Both ≤ 10, you rank higher Monitor
Shared, behind Both ≤ 10, competitor higher Improve the page; compare their ranking URL
Near gap Competitor ≤ 10, you 11–20 Fastest wins: you already have a page Google considers
Gap Competitor ≤ 10, you absent within depth New page or section
Paid only Competitor ads present, nobody tracked ranks Organic opportunity with proven intent
Untouched Nobody tracked ranks or advertises Low priority unless volume says otherwise
def classify(row, you="you", rival="billfox"):
    y, r = row["best"].get(you, {}).get("rank"), row["best"].get(rival, {}).get("rank")
    if r and r <= 10 and not y:
        return "gap"
    if r and r <= 10 and y and y > 10:
        return "near_gap"
    if y and y <= 10 and (not r or r > y):
        return "strength" if not r or r > 10 else "shared_ahead"
    if y and r and y <= 10 and r < y:
        return "shared_behind"
    if rival in row["advertisers"] and not r:
        return "paid_only"
    return "untouched"

Overlap between you and a rival = keywords where both rank ≤ 10 ÷ keywords where either does. It tells you how directly you compete in search, which is often different from how directly you compete in sales.

Adding volume and difficulty

Result pages tell you who ranks; they do not tell you whether anyone searches. Before prioritising the gap list, join a demand source:

A simple priority score once volume is joined:

priority = volume × intent_weight × gap_weight

with gap_weight highest for near gaps (a page exists) and paid-only keywords (intent proven by spend), and intent_weight set per keyword group. Keep keyword difficulty, if you use one, as a separate column from the vendor that produced it; it is a model, not an observation.

Cost

A Google Search async task costs 3 credits for one page and 2 more per extra page. Synchronous calls add 2.

Worked example for two competitors in one market:

Stage Tasks Credits per task Credits
80 seeds, 2 pages 80 5 400
1,200 filtered expansion candidates, 2 pages 1,200 5 6,000
40 site: probes, 1 page 40 3 120
150 paid-intent keywords, 2 extra ad samples, 1 page 300 3 900
Total 7,420

Re-running the same candidate set monthly to watch the gap close costs the 6,400 credits of the first two rows again, or 3,840 at one page. A free account’s 500 monthly credits cover the 80-seed run and a handful of probes, enough to validate the matching code. See pricing.

Pitfalls

To keep watching these keywords after the analysis, see competitor SEO tracking. The request and response fields are on the Google Search engine page.

Questions

How do I find which keywords a competitor ranks for?

Build a candidate keyword set, run each keyword through Google Search, and record every keyword where a result in organicResults links to the competitor's domain. The result is the list of your candidates they rank for, with their position, in the market and device you requested.

Does the API return search volume or keyword difficulty?

No. It returns the live results page: organic results, ads, People Also Ask, related searches and other features. Pair it with a volume source such as Google Keyword Planner, or your own Search Console data for keywords your site already appears for.

Can I see every keyword a competitor ranks for?

Not from result pages alone. You only see keywords you query. Commercial keyword databases estimate a full list from their own crawls; the SERP approach gives exact, current positions for the candidates you choose, in any market.

How do I find the keywords a competitor bids on?

Run your candidate keywords and read result.ads, where each text ad has a domain and blockPosition. Keywords where the competitor's domain appears across repeated samples are keywords they are bidding on in that market.

What does a site: query show about a competitor?

Google interprets operators in the query, so site:competitor.com plus a topic returns pages Google associates with that topic on their domain. It is a sample of what Google chooses to show, not a list of every page or keyword.

Try it on your own prompts

500 free credits a month, no card. One POST returns the answer, sources and citations as JSON.

Keep reading