DEV Community

Cecilia Hill
Cecilia Hill

Posted on

How to Track People Also Ask Results with a SERP API

People Also Ask results are easy to ignore.

They sit in the middle of Google results, looking like a small accordion of questions.

But for SEO, content research, and AI search workflows, they are useful.

They show what people are asking around a topic.

Not what a keyword tool thinks they might ask.

Not what your content calendar guessed three months ago.

Actual questions appearing inside search results.

If you track them over time, you can see:

new questions appearing
old questions disappearing
competitor topics expanding
content gaps
question patterns by location
changes in search intent
Enter fullscreen mode Exit fullscreen mode

In this article, we will build a small Python script that tracks People Also Ask results with a SERP API.

The goal is simple:

keyword → SERP API → People Also Ask questions → save snapshot → compare changes
Enter fullscreen mode Exit fullscreen mode

No giant SEO platform.

Just a practical script you can understand, run, and modify.

What are People Also Ask results?

People Also Ask, often shortened to PAA, is the block of related questions that appears on many Google results pages.

For example, a query like:

best project management software
Enter fullscreen mode Exit fullscreen mode

might show questions such as:

What is the best project management tool?
Which project management software is easiest to use?
Is Trello better than Asana?
What do small teams use for project management?
Enter fullscreen mode Exit fullscreen mode

These questions are useful because they reveal the searcher's next question.

That is the good stuff.

A keyword tells you what someone typed.

People Also Ask tells you what they may want to understand next.

Why track PAA results?

A single PAA snapshot is useful.

Tracking PAA over time is better.

If you collect PAA questions every day or every week, you can answer questions like:

Which questions keep appearing?
Which new questions appeared this week?
Which topics are growing?
Which questions should we answer in content?
Which questions should an AI assistant handle?
Which competitors appear around these questions?
Enter fullscreen mode Exit fullscreen mode

This is useful for:

  • content planning
  • SEO research
  • featured snippet research
  • competitor monitoring
  • topic clustering
  • AI agent search context
  • FAQ generation
  • market research

PAA data is not a magic oracle.

But it is a useful window into search intent. A slightly noisy window, yes, but still better than guessing in a dark room with a spreadsheet candle.

What we will build

We will write a Python script that:

  1. Reads a list of keywords
  2. Calls a SERP API for each keyword
  3. Extracts People Also Ask questions
  4. Normalizes the response
  5. Saves results to CSV
  6. Saves snapshots by date
  7. Compares today's questions with a previous snapshot
  8. Finds new and removed questions

The output will look like this:

keyword,question,answer_snippet,source_title,source_url,date
best project management software,What is the best project management software?,...,example.com,2026-01-01
Enter fullscreen mode Exit fullscreen mode

The exact API response shape depends on your SERP API provider.

Common field names include:

people_also_ask
related_questions
paa_results
questions
Enter fullscreen mode Exit fullscreen mode

So we will write the parser defensively.

Install dependencies

Create a new folder and install the packages:

pip install requests python-dotenv pandas
Enter fullscreen mode Exit fullscreen mode

We will use:

requests → call the API
python-dotenv → load API keys
pandas → save and compare CSV files
Enter fullscreen mode Exit fullscreen mode

Create a .env file

Create a .env file:

SERP_API_KEY=your_api_key
SERP_API_URL=https://your-serp-api-endpoint.example.com/search
Enter fullscreen mode Exit fullscreen mode

This article uses a generic SERP API request format.

Your provider may use different parameter names.

For example, some providers use:

q
query
engine
location
gl
hl
device
Enter fullscreen mode Exit fullscreen mode

Always check your provider's docs and adjust the request function.

Create a keyword file

Create keywords.txt:

best project management software
crm software for small business
google search api
serp api
ai search agent
Enter fullscreen mode Exit fullscreen mode

Use real keywords from your workflow.

Do not only test clean examples.

PAA gets interesting when queries are commercial, local, or messy.

Step 1: Call the SERP API

Create a file called track_paa.py.

import os
import requests
from dotenv import load_dotenv


load_dotenv()

SERP_API_KEY = os.getenv("SERP_API_KEY")
SERP_API_URL = os.getenv("SERP_API_URL")


def fetch_serp(query, location="United States", language="en"):
    if not SERP_API_KEY:
        raise ValueError("Missing SERP_API_KEY")

    if not SERP_API_URL:
        raise ValueError("Missing SERP_API_URL")

    params = {
        "api_key": SERP_API_KEY,
        "engine": "google",
        "q": query,
        "location": location,
        "language": language,
        "output": "json",
    }

    response = requests.get(
        SERP_API_URL,
        params=params,
        timeout=30,
    )

    response.raise_for_status()
    return response.json()
Enter fullscreen mode Exit fullscreen mode

This function returns raw SERP JSON.

Keep it separate from the parser.

When something breaks, you want to know whether the API call failed or your parser failed.

Debugging soup is bad. Debugging soup with no labels is worse.

Step 2: Load keywords

Add a helper to load keywords from a text file.

def load_keywords(filename="keywords.txt"):
    with open(filename, "r", encoding="utf-8") as file:
        return [
            line.strip()
            for line in file
            if line.strip()
        ]
Enter fullscreen mode Exit fullscreen mode

This lets you update keyword lists without touching code.

Step 3: Find the PAA block

Different APIs may return PAA data under different keys.

Let's support several possible shapes.

def get_paa_items(data):
    possible_keys = [
        "people_also_ask",
        "related_questions",
        "paa_results",
        "questions",
    ]

    for key in possible_keys:
        value = data.get(key)

        if isinstance(value, list):
            return value

    return []
Enter fullscreen mode Exit fullscreen mode

This is intentionally simple.

You can expand it later if your provider nests PAA inside another object.

For example:

def get_paa_items(data):
    possible_keys = [
        "people_also_ask",
        "related_questions",
        "paa_results",
        "questions",
    ]

    for key in possible_keys:
        value = data.get(key)

        if isinstance(value, list):
            return value

    serp = data.get("serp", {})

    for key in possible_keys:
        value = serp.get(key)

        if isinstance(value, list):
            return value

    return []
Enter fullscreen mode Exit fullscreen mode

Use the version that matches your API response.

Step 4: Normalize PAA items

A PAA item may include a question, answer snippet, title, link, and sometimes more fields.

We will normalize each item into one stable format.

def normalize_paa_item(keyword, item, date_string):
    question = (
        item.get("question")
        or item.get("title")
        or item.get("query")
        or ""
    )

    answer_snippet = (
        item.get("snippet")
        or item.get("answer")
        or item.get("description")
        or ""
    )

    source_title = (
        item.get("source_title")
        or item.get("title")
        or ""
    )

    source_url = (
        item.get("link")
        or item.get("url")
        or item.get("source_url")
        or ""
    )

    return {
        "date": date_string,
        "keyword": keyword,
        "question": clean_text(question),
        "answer_snippet": clean_text(answer_snippet),
        "source_title": clean_text(source_title),
        "source_url": source_url,
    }
Enter fullscreen mode Exit fullscreen mode

Add a small text cleaner:

import re


def clean_text(value):
    if not value:
        return ""

    if not isinstance(value, str):
        value = str(value)

    value = re.sub(r"\s+", " ", value)
    return value.strip()
Enter fullscreen mode Exit fullscreen mode

This removes weird spacing.

Small cleanup now saves annoying CSV goblins later.

Step 5: Remove weak rows

Sometimes the API may return an item without a real question.

Skip those.

def is_valid_paa_row(row):
    if not row["question"]:
        return False

    if len(row["question"]) < 5:
        return False

    return True
Enter fullscreen mode Exit fullscreen mode

For content research, the question is the core field.

If there is no question, the row is not useful.

Step 6: Deduplicate questions

The same question may appear more than once.

Deduplicate by keyword plus normalized question.

def normalize_question_key(question):
    question = question.lower().strip()
    question = re.sub(r"[^\w\s]", "", question)
    question = re.sub(r"\s+", " ", question)
    return question


def dedupe_paa_rows(rows):
    seen = set()
    unique_rows = []

    for row in rows:
        key = (
            row["keyword"].lower().strip(),
            normalize_question_key(row["question"]),
        )

        if key in seen:
            continue

        seen.add(key)
        unique_rows.append(row)

    return unique_rows
Enter fullscreen mode Exit fullscreen mode

This makes comparison cleaner.

Without dedupe, your snapshot can get noisy fast.

Step 7: Fetch PAA for one keyword

Now combine the API call and parser.

from datetime import date


def fetch_paa_for_keyword(keyword, location="United States", language="en"):
    date_string = date.today().isoformat()

    data = fetch_serp(
        query=keyword,
        location=location,
        language=language,
    )

    paa_items = get_paa_items(data)

    rows = [
        normalize_paa_item(keyword, item, date_string)
        for item in paa_items
    ]

    rows = [
        row
        for row in rows
        if is_valid_paa_row(row)
    ]

    return dedupe_paa_rows(rows)
Enter fullscreen mode Exit fullscreen mode

Now one keyword gives us clean rows.

Step 8: Fetch all keywords

Add a loop for the full keyword list.

import time


def fetch_all_paa(keywords, location="United States", language="en", delay=1):
    all_rows = []

    for keyword in keywords:
        print(f"Fetching PAA for: {keyword}")

        try:
            rows = fetch_paa_for_keyword(
                keyword=keyword,
                location=location,
                language=language,
            )

            all_rows.extend(rows)

        except Exception as exc:
            print(f"Failed keyword: {keyword}")
            print(f"Error: {exc}")

        time.sleep(delay)

    return all_rows
Enter fullscreen mode Exit fullscreen mode

The delay is there to avoid hitting APIs too aggressively.

Respect rate limits.

A script that gets rate-limited in five seconds is not automation. It is a tiny cannon with billing attached.

Step 9: Save a daily snapshot

Now save the results as CSV.

import pandas as pd


def save_snapshot(rows):
    today = date.today().isoformat()
    filename = f"paa_snapshot_{today}.csv"

    df = pd.DataFrame(rows)
    df.to_csv(filename, index=False)

    print(f"Saved snapshot: {filename}")

    return filename
Enter fullscreen mode Exit fullscreen mode

Every time you run the script, you get a file like:

paa_snapshot_2026-01-01.csv
Enter fullscreen mode Exit fullscreen mode

That is useful for weekly or monthly comparison.

Full tracking script

Here is the full version.

import os
import re
import time
import requests
import pandas as pd
from datetime import date
from dotenv import load_dotenv


load_dotenv()

SERP_API_KEY = os.getenv("SERP_API_KEY")
SERP_API_URL = os.getenv("SERP_API_URL")


def clean_text(value):
    if not value:
        return ""

    if not isinstance(value, str):
        value = str(value)

    value = re.sub(r"\s+", " ", value)
    return value.strip()


def normalize_question_key(question):
    question = question.lower().strip()
    question = re.sub(r"[^\w\s]", "", question)
    question = re.sub(r"\s+", " ", question)
    return question


def fetch_serp(query, location="United States", language="en"):
    if not SERP_API_KEY:
        raise ValueError("Missing SERP_API_KEY")

    if not SERP_API_URL:
        raise ValueError("Missing SERP_API_URL")

    params = {
        "api_key": SERP_API_KEY,
        "engine": "google",
        "q": query,
        "location": location,
        "language": language,
        "output": "json",
    }

    response = requests.get(
        SERP_API_URL,
        params=params,
        timeout=30,
    )

    response.raise_for_status()
    return response.json()


def load_keywords(filename="keywords.txt"):
    with open(filename, "r", encoding="utf-8") as file:
        return [
            line.strip()
            for line in file
            if line.strip()
        ]


def get_paa_items(data):
    possible_keys = [
        "people_also_ask",
        "related_questions",
        "paa_results",
        "questions",
    ]

    for key in possible_keys:
        value = data.get(key)

        if isinstance(value, list):
            return value

    serp = data.get("serp", {})

    if isinstance(serp, dict):
        for key in possible_keys:
            value = serp.get(key)

            if isinstance(value, list):
                return value

    return []


def normalize_paa_item(keyword, item, date_string):
    question = (
        item.get("question")
        or item.get("title")
        or item.get("query")
        or ""
    )

    answer_snippet = (
        item.get("snippet")
        or item.get("answer")
        or item.get("description")
        or ""
    )

    source_title = (
        item.get("source_title")
        or item.get("title")
        or ""
    )

    source_url = (
        item.get("link")
        or item.get("url")
        or item.get("source_url")
        or ""
    )

    return {
        "date": date_string,
        "keyword": keyword,
        "question": clean_text(question),
        "answer_snippet": clean_text(answer_snippet),
        "source_title": clean_text(source_title),
        "source_url": source_url,
    }


def is_valid_paa_row(row):
    if not row["question"]:
        return False

    if len(row["question"]) < 5:
        return False

    return True


def dedupe_paa_rows(rows):
    seen = set()
    unique_rows = []

    for row in rows:
        key = (
            row["keyword"].lower().strip(),
            normalize_question_key(row["question"]),
        )

        if key in seen:
            continue

        seen.add(key)
        unique_rows.append(row)

    return unique_rows


def fetch_paa_for_keyword(keyword, location="United States", language="en"):
    date_string = date.today().isoformat()

    data = fetch_serp(
        query=keyword,
        location=location,
        language=language,
    )

    paa_items = get_paa_items(data)

    rows = [
        normalize_paa_item(keyword, item, date_string)
        for item in paa_items
    ]

    rows = [
        row
        for row in rows
        if is_valid_paa_row(row)
    ]

    return dedupe_paa_rows(rows)


def fetch_all_paa(keywords, location="United States", language="en", delay=1):
    all_rows = []

    for keyword in keywords:
        print(f"Fetching PAA for: {keyword}")

        try:
            rows = fetch_paa_for_keyword(
                keyword=keyword,
                location=location,
                language=language,
            )

            all_rows.extend(rows)

        except Exception as exc:
            print(f"Failed keyword: {keyword}")
            print(f"Error: {exc}")

        time.sleep(delay)

    return all_rows


def save_snapshot(rows):
    today = date.today().isoformat()
    filename = f"paa_snapshot_{today}.csv"

    df = pd.DataFrame(rows)
    df.to_csv(filename, index=False)

    print(f"Saved snapshot: {filename}")

    return filename


def main():
    keywords = load_keywords("keywords.txt")

    rows = fetch_all_paa(
        keywords=keywords,
        location="United States",
        language="en",
        delay=1,
    )

    save_snapshot(rows)

    print(f"Collected {len(rows)} PAA rows.")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it:

python track_paa.py
Enter fullscreen mode Exit fullscreen mode

You should get a CSV snapshot.

Compare two snapshots

Tracking becomes useful when you compare snapshots.

Let's say you have:

paa_snapshot_2026-01-01.csv
paa_snapshot_2026-01-08.csv
Enter fullscreen mode Exit fullscreen mode

Create a new file called compare_paa_snapshots.py.

import re
import pandas as pd


def normalize_question_key(question):
    question = str(question).lower().strip()
    question = re.sub(r"[^\w\s]", "", question)
    question = re.sub(r"\s+", " ", question)
    return question


def add_compare_key(df):
    df = df.copy()

    df["question_key"] = df["question"].apply(normalize_question_key)

    df["compare_key"] = (
        df["keyword"].str.lower().str.strip()
        + "||"
        + df["question_key"]
    )

    return df


def compare_snapshots(old_file, new_file):
    old_df = pd.read_csv(old_file)
    new_df = pd.read_csv(new_file)

    old_df = add_compare_key(old_df)
    new_df = add_compare_key(new_df)

    old_keys = set(old_df["compare_key"])
    new_keys = set(new_df["compare_key"])

    added_keys = new_keys - old_keys
    removed_keys = old_keys - new_keys
    unchanged_keys = old_keys & new_keys

    added = new_df[new_df["compare_key"].isin(added_keys)]
    removed = old_df[old_df["compare_key"].isin(removed_keys)]
    unchanged = new_df[new_df["compare_key"].isin(unchanged_keys)]

    return added, removed, unchanged


def main():
    old_file = "paa_snapshot_2026-01-01.csv"
    new_file = "paa_snapshot_2026-01-08.csv"

    added, removed, unchanged = compare_snapshots(old_file, new_file)

    added.to_csv("paa_added.csv", index=False)
    removed.to_csv("paa_removed.csv", index=False)
    unchanged.to_csv("paa_unchanged.csv", index=False)

    print(f"Added questions: {len(added)}")
    print(f"Removed questions: {len(removed)}")
    print(f"Unchanged questions: {len(unchanged)}")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run:

python compare_paa_snapshots.py
Enter fullscreen mode Exit fullscreen mode

Now you get:

paa_added.csv
paa_removed.csv
paa_unchanged.csv
Enter fullscreen mode Exit fullscreen mode

This is where the PAA tracking starts to become useful.

What to do with new PAA questions

New PAA questions can mean several things.

They may show:

new search intent
new concerns
new comparison angles
new competitor awareness
new product category language
new educational gaps
Enter fullscreen mode Exit fullscreen mode

For content teams, new PAA questions can become:

  • FAQ sections
  • article headings
  • comparison page ideas
  • support docs
  • glossary entries
  • video scripts
  • social post topics

For AI teams, they can become:

  • evaluation questions
  • agent test prompts
  • retrieval queries
  • topic expansion data

For SEO teams, they can become:

  • keyword clusters
  • on-page optimization ideas
  • featured snippet targets
  • content refresh signals

Not every PAA question deserves content.

Some are too broad.

Some are repetitive.

Some are weird little search mushrooms.

But repeated PAA questions are worth watching.

Group questions by keyword

You can quickly count how many PAA questions each keyword returns.

df = pd.read_csv("paa_snapshot_2026-01-08.csv")

summary = (
    df.groupby("keyword")
    .size()
    .reset_index(name="paa_count")
    .sort_values("paa_count", ascending=False)
)

summary.to_csv("paa_keyword_summary.csv", index=False)

print(summary)
Enter fullscreen mode Exit fullscreen mode

This helps you find which topics have more question depth.

A keyword with many PAA results may be good for educational content.

A keyword with no PAA results may still matter, but it may be more direct or transactional.

Group questions by words

A simple word count can reveal repeated patterns.

from collections import Counter


def tokenize(text):
    text = str(text).lower()
    text = re.sub(r"[^\w\s]", "", text)

    stopwords = {
        "what", "how", "is", "are", "the", "a", "an",
        "to", "for", "of", "and", "in", "with", "does",
    }

    words = [
        word
        for word in text.split()
        if word not in stopwords and len(word) > 2
    ]

    return words


df = pd.read_csv("paa_snapshot_2026-01-08.csv")

counter = Counter()

for question in df["question"]:
    counter.update(tokenize(question))

print(counter.most_common(20))
Enter fullscreen mode Exit fullscreen mode

This is basic, but useful.

If words like:

pricing
alternative
free
compare
best
tools
api
Enter fullscreen mode Exit fullscreen mode

keep appearing, you know what users are circling around.

Search intent leaves fingerprints.

Add location tracking

People Also Ask can change by location.

If your SERP API supports location parameters, track them.

Modify rows to include location:

def normalize_paa_item(keyword, item, date_string, location):
    question = (
        item.get("question")
        or item.get("title")
        or item.get("query")
        or ""
    )

    answer_snippet = (
        item.get("snippet")
        or item.get("answer")
        or item.get("description")
        or ""
    )

    source_title = (
        item.get("source_title")
        or item.get("title")
        or ""
    )

    source_url = (
        item.get("link")
        or item.get("url")
        or item.get("source_url")
        or ""
    )

    return {
        "date": date_string,
        "location": location,
        "keyword": keyword,
        "question": clean_text(question),
        "answer_snippet": clean_text(answer_snippet),
        "source_title": clean_text(source_title),
        "source_url": source_url,
    }
Enter fullscreen mode Exit fullscreen mode

Then your comparison key should include location:

df["compare_key"] = (
    df["location"].str.lower().str.strip()
    + "||"
    + df["keyword"].str.lower().str.strip()
    + "||"
    + df["question_key"]
)
Enter fullscreen mode Exit fullscreen mode

This is useful for local SEO and regional content planning.

Save to SQLite instead of CSV

CSV is fine at the beginning.

Once snapshots pile up, use SQLite.

import sqlite3


def save_to_sqlite(rows, database="paa_tracking.db"):
    df = pd.DataFrame(rows)

    with sqlite3.connect(database) as connection:
        df.to_sql(
            "paa_results",
            connection,
            if_exists="append",
            index=False,
        )
Enter fullscreen mode Exit fullscreen mode

Then you can query historical data:

SELECT keyword, question, COUNT(*) as appearances
FROM paa_results
GROUP BY keyword, question
ORDER BY appearances DESC;
Enter fullscreen mode Exit fullscreen mode

This helps find persistent questions.

Persistent questions are usually more valuable than one-day surprises.

A note on SERP API providers

Most SERP API providers can return some form of Google result data, but the response shapes differ.

When testing providers, check:

Does it return People Also Ask data?
What is the field name?
Does it include answer snippets?
Does it include source URLs?
Does location targeting work?
Are empty PAA blocks common?
Is the response easy to normalize?
Enter fullscreen mode Exit fullscreen mode

Common mistakes

Only testing one keyword

One keyword proves very little.

Test at least 20 to 50 keywords.

Include:

commercial queries
informational queries
comparison queries
branded queries
local queries
long-tail queries
Enter fullscreen mode Exit fullscreen mode

Treating every PAA question as content-worthy

Some questions are not worth a full article.

Some belong in a FAQ section.

Some belong inside an existing post.

Some are just noise wearing a question mark.

Ignoring location

If your audience is local or regional, test location-specific SERPs.

PAA can change across countries and cities.

Not saving snapshots

If you only look at today's results, you cannot detect changes.

Save snapshots.

The value is in the history.

Sending PAA data directly to an LLM without cleaning

If you use PAA data for AI workflows, clean it first.

Use a source-aware format:

Question [1]
Keyword: serp api
Question: What is a SERP API?
Source URL: https://example.com
Snippet: ...
Enter fullscreen mode Exit fullscreen mode

Do not dump raw JSON into the prompt unless you enjoy token confetti.

Final thoughts

People Also Ask tracking is not complicated.

The basic workflow is:

keywords
→ SERP API
→ PAA questions
→ clean rows
→ daily snapshot
→ compare changes
Enter fullscreen mode Exit fullscreen mode

The useful part is not one export.

The useful part is watching the questions change over time.

New questions can reveal fresh intent.

Repeated questions can reveal stable content opportunities.

Removed questions can show shifting SERP behavior.

For SEO, PAA tracking helps with content planning and search intent research.

For AI apps, it can help generate better test prompts and search-grounded answer flows.

Start small.

Use 20 keywords.

Collect once a week.

Save the CSV.

Compare snapshots.

Once the pattern is working, add locations, SQLite, alerts, and dashboards.

Search intent moves quietly. PAA tracking gives you a small radar.

Top comments (0)