AnswerLine Start free

, SERP API · Google · Tutorials

Turn Google results into typed data, not HTML scraping

A Google results page holds many datasets in one document: organic rankings, ads, questions, a local pack, a knowledge panel, product grids and an AI Overview. Parsing each from HTML means maintaining a parser per block. Below: the request fields that shape the page, the blocks the response returns, how to model them in Python and TypeScript, and the normalization details that catch first integrations.

Structured SERP data is a search results page delivered as typed fields, one array or object per block, with named properties for position, title, link and the block’s own attributes, rather than as markup you parse yourself.

One call, every block

curl -X POST https://api.answerline.dev/v1/monitor/google \
  -H "Authorization: Bearer $ANSWERLINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "plumber austin tx",
    "country": "US",
    "include": { "aioverview": {} }
  }'
{
  "success": true,
  "result": {
    "organicResults": [...],
    "peopleAlsoAsk": [...],
    "relatedSearches": [...],
    "ads": [...],
    "localResults": [...],
    "localResultsMoreLink": "...",
    "knowledgeGraph": {...},
    "shoppingCards": [...],
    "peopleAreSaying": [...],
    "aioverview": {...}
  }
}

Not every block appears on every page; the next sections say which are omitted when absent.

Shaping the request

The endpoint accepts one of three shapes: query with country, query with gl, or a complete Google search url. Everything else is optional.

Field What it does Notes
query The search to run Required unless you send url
country / gl Result country, ISO 3166-1 alpha-2 Either name works; different values in both is a 400. GET /v1/countries lists supported codes
pages 1 to 10 results pages Each page after the first adds $0.00071
include.html Adds html[], a link to a copy of each results page No extra cost
include.aioverview Adds aioverview; an object, {"markdown": true} adds markdown Presence enables it; adds $0.00071
include.paaAioverview Fills AI-Overview-type PAA items with markdown and sources Free; slower responses
url Runs an exact Google search URL Exclusive with query and pages; the URL may carry only q, gl, num, hl and uule

location (e.g. Austin,Texas,United States) or uule targets a city.

For news results use the separate POST /v1/monitor/google/news endpoint; see getting Google News data.

Defaults that suit most integrations: send country; request one page unless you need positions beyond 10; add include.aioverview only where you will read it, because it changes the price.

The blocks, field by field

Organic results

organicResults[] carries position (1-indexed), title, link, displayedLink, snippet, date when Google shows one, page (which results page it came from) and sitelinks.inline[] with title and link. With pages above 1, use page to tell page-one results from deeper ones.

Ads

ads[] holds both text ads (type: "RESULT") and shopping-style sponsored cards (type: "SHOPPING_CARD"). Each has position within its block, blockPosition (top, bottom, middle or rhs), title, url, displayedUrl, domain, description, page and sitelinks[]. Shopping cards add category (for example a “Sponsored products” carousel header), price and oldPrice objects, store and imageUrl.

peopleAlsoAsk[] items have question and type: LINK items carry snippet, title and link; AIOVERVIEW items carry markdown and sources[] when you set include.paaAioverview; UNKNOWN covers the rest. relatedSearches[] has query and link. Both feed keyword research, as in mining People Also Ask.

Local pack

localResults[] holds the map-backed pack on local-intent queries: position, title, placeId, rating, reviews, price, type, yearsInBusiness, address, phone, hours, description, and links with website and directions on service-style packs. localResultsMoreLink points to Google’s expanded list when the page offers one. The block is omitted when there is no pack.

Knowledge panel

knowledgeGraph is an object with title, type, kgmid, description, website, source, attributes[] (key, value), profiles[] and many entity-specific arrays, such as ratings[] for films, webRatings[] and contact[] for local businesses, hotelOptions[] for hotels, and statsCard for teams. It is omitted when no panel appears.

Shopping grid and discussions

shoppingCards[] comes from organic product grids: title, position (flat across the whole array), productLink, promoLabel, category, price, oldPrice, store, rating, reviews and thumbnail. peopleAreSaying[] is the discussions module: position, title, link (often a forum thread) and date. Both are omitted when absent.

AI Overview

With include.aioverview, aioverview is null when the page had no overview, or an object with text, markdown (if requested), sources[], citationPills[] (group by citationPillId to rebuild chips), relatedLinks[], videos[] and ads[]. Monitoring Google AI Overviews covers what to measure from it.

Modeling it in code

Type only the fields you read, and mark every block that can be omitted as optional. In TypeScript:

import { Client } from "@answerline/sdk";

interface Organic { position: number; title: string; link: string; displayedLink?: string; snippet?: string; page?: number; redirectLink?: string }
interface Ad { position: number; blockPosition?: "top" | "bottom" | "middle" | "rhs"; type?: "RESULT" | "SHOPPING_CARD"; title?: string; url?: string; domain?: string }
interface Paa { question: string; type: "AIOVERVIEW" | "LINK" | "UNKNOWN"; link?: string; snippet?: string }
interface LocalPlace { position: number; title: string; placeId?: string; rating?: number; reviews?: string; address?: string; links?: { website?: string } }

interface GoogleResult {
  organicResults?: Organic[];
  ads?: Ad[];
  peopleAlsoAsk?: Paa[];
  relatedSearches?: { query: string; link: string }[];
  localResults?: LocalPlace[];
  knowledgeGraph?: { title?: string; kgmid?: string; website?: string };
  aioverview?: { text?: string; sources?: { position: number; url: string; label: string }[] } | null;
}

const client = new Client({ apiKey: process.env.ANSWERLINE_API_KEY! });
const { result } = await client.monitor.google<GoogleResult>({ query: "plumber austin tx", country: "US" });

const topThree = (result.organicResults ?? []).filter((r) => r.page === 1).slice(0, 3).map((r) => r.link);
const hasPack = result.localResults !== undefined;
const topAds = (result.ads ?? []).filter((a) => a.blockPosition === "top").length;

In Python, flatten the blocks you report on into rows as soon as the response arrives, so downstream code never touches the nested JSON:

import os
import requests

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

def search(query: str, country: str, **extra) -> dict:
    res = requests.post(f"{API}/v1/monitor/google", json={"query": query, "country": country, **extra},
                        headers=HEADERS, timeout=(10, 330))
    res.raise_for_status()
    return res.json()["result"]

def rows(result: dict, query: str, country: str) -> list[dict]:
    out = []
    for r in result.get("organicResults") or []:
        out.append({"query": query, "country": country, "block": "organic", "position": r["position"],
                    "page": r.get("page"), "url": r["link"], "title": r.get("title")})
    for a in result.get("ads") or []:
        out.append({"query": query, "country": country, "block": f"ad:{a.get('blockPosition')}",
                    "position": a.get("position"), "page": a.get("page"), "url": a.get("url"), "title": a.get("title")})
    for p in result.get("localResults") or []:
        out.append({"query": query, "country": country, "block": "local", "position": p["position"],
                    "page": None, "url": (p.get("links") or {}).get("website"), "title": p["title"]})
    return out

result = search("plumber austin tx", "US", pages=2)
for row in rows(result, "plumber austin tx", "US")[:5]:
    print(row)

One row shape across blocks makes “who occupies this page” a single query, and adding a block later is one more loop.

Normalization details

A few values are returned as display text rather than normalized numbers. Handle them once, at ingestion:

Many link fields have a sibling redirectLink when Google served the link as a redirect; the link field itself already holds the destination. When you only have the redirect, POST /v1/monitor/google/goto with {"url": "<redirectLink>"} returns {"url": "<destination>"} and is free.

Typed data versus parsing pages yourself

For a team deciding whether to parse pages itself:

The trade-off is cost per request and dependence on a provider; build vs buy lays that out.

Cost and volume

Cost per request at the Starter tier:

Request Price
1 page $0.00103
1 page + include.aioverview $0.00174
pages: 3 + include.aioverview $0.00103 + 2 × $0.00071 + $0.00071 = $0.00316

Page charges depend on the pages returned and never exceed the reserved maximum. For keyword lists, submit GOOGLE tasks in batches of up to 500 and collect them by webhook, as in the rank-tracking pipeline and tracking AI Overviews with async batches; sync, async or webhooks helps choose. Prices on larger tiers are on the pricing page.

Uses

Full field shapes are on the Google Search engine page; the quickstart covers a first call on the engines served today.

Questions

What is structured SERP data?

It is a search results page delivered as typed fields instead of HTML: each block, such as organic results, ads or the local pack, arrives as its own array or object with named fields. You read positions, links and snippets directly instead of parsing markup.

Which request fields control geography?

country or gl sets the result country, and location or uule a city, such as Austin,Texas,United States.

Do I need separate requests for organic results, ads and the AI Overview?

No. One request returns organicResults, ads, peopleAlsoAsk, relatedSearches, localResults and knowledgeGraph for the same results page, and the AI Overview arrives in the same response when include.aioverview is part of that request.

How do I resolve a Google redirectLink to its destination?

Send it to POST /v1/monitor/google/goto as {"url": ...}; the response returns the destination as url. That endpoint is free.

Why is a field like localResults or knowledgeGraph missing from some responses?

Several blocks, including localResults, shoppingCards, knowledgeGraph and peopleAreSaying, are omitted from result when the page does not show them. Check whether the key exists rather than expecting an empty array.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading