News Monitoring API
Google News results for any query, country and language, up to ten pages deep, on whatever schedule you set. Pair it with an AI engine monitor and you also see what the assistants say when someone asks about the story.
Who it's for
Media-monitoring platforms, comms and PR teams, finance and research products tracking moving stories.
What you get
- Headline, publisher, timestamp and link per result, parsed and ordered
- Country and interface-language parameters for regional coverage
- Async tasks with webhooks, so each check lands in your pipeline when it finishes
- The same entity monitored in AI answers, to catch wrong claims early
Try it
curl -X POST "https://api.answerline.dev/v1/monitor/google/news" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"electric vehicle battery","country":"FR","hl":"fr"}'
Engines for this workload
From alerts to a coverage dataset
An email alert says something was published. A monitoring pipeline measures how much was published, by whom, how fast it spread, whether it is still growing, and what assistants say about it. That takes one row per article, one row per sighting, and collection on a fixed schedule.
Google News results come back as result.newsResults, each with title, link, snippet, source, date, position and page, which is enough to build a coverage dataset for any entity, topic or story, per market and language.
Metrics
| Metric | Definition |
|---|---|
| Coverage volume | Distinct articles (by normalized URL) first seen for a query in a period. |
| Publisher reach | Distinct source values among those articles. |
| Tier-1 share | Articles from a publisher list you define as priority ÷ all articles. |
| News share of voice | Articles matching entity E ÷ articles matching any entity in a tracked set, over the same queries and period. |
| Velocity | New articles per hour over a sliding window. A story is accelerating when velocity rises across consecutive windows. |
| Time to first sighting | First time an article appears in your data − its estimated publication time. Bounded below by your polling interval. |
| Prominence | Share of checks in which an article held position ≤ 3 on page 1 while it was visible. |
| AI answer coverage | Answers to a question about the story that cite at least one article from your coverage set ÷ answers collected. |
Keep “first seen” and “estimated published” as separate columns. First seen is exact to your capture; published is derived from a display string and approximate.
Architecture
entities & queries ─> scheduler (per cadence tier) ─> batch ─> webhook
│
articles (unique URL) <── upsert ◄────┤
sightings (per check) <───────────────┘
│
new-article alerts, velocity, AI answer checks
Query design
- Quote exact names:
"Acme Robotics"rather thanacme robotics. Google’s phrase and exclusion operators apply, so check a sample of results whenever you add a query. - Write one query per variant that matters (legal name, brand name, ticker, product names, executive names) and map each to an entity id.
- Exclude known collisions:
"Acme" -cartoon. Keep exclusions in data, not code. - Add topic queries for stories without a single entity (“EU battery regulation”), tagged to a topic id.
- Set markets with
countryorglfor geography andhlfor language. A French check iscountry: "FR", hl: "fr", with the query in French where the name is translated. - Choose depth:
pagesaccepts 1 to 10. Page 1 covers prominence; deeper pages add recall for high-volume entities at extra cost.
Cadence tiers
| Tier | Cadence | Typical members |
|---|---|---|
| Hot | Hourly | Active incidents, launches, the week of earnings |
| Warm | Every 4 to 6 hours | Your brand, top competitors, executives |
| Background | Daily | Topics, secondary products, partners |
Promote an entity to hot automatically when velocity crosses a threshold, and demote it after a quiet period. The idempotency key includes the check slot, so a restart inside the same hour submits nothing new:
news:{query_id}:{country}:{hl}:{yyyy-mm-ddThh}
Each check is a GOOGLE_NEWS task. Give hot-tier tasks a higher priority (1 to 10, higher first) so they overtake background work in your queue. Batch, delivery and retry behavior is in async tasks and webhooks; the AI visibility tracking guide has a complete receiver.
Storage schema
create table queries (
query_id text primary key,
entity_id text not null,
q text not null,
country text not null,
hl text,
pages int not null default 1,
tier text not null default 'warm'
);
create table checks (
task_id uuid primary key,
query_id text not null references queries,
slot timestamptz not null,
status text not null default 'QUEUED',
captured_at timestamptz,
unique (query_id, slot)
);
create table articles (
url_key text primary key, -- sha256 of normalized URL
url text not null,
title text not null,
source text,
snippet text,
first_seen timestamptz not null,
est_published timestamptz,
raw_date text
);
create table sightings (
task_id uuid references checks,
url_key text references articles,
entity_id text not null,
page int,
position int,
primary key (task_id, url_key)
);
Deduplication
Deduplicate at two levels:
- Deliveries:
checks.task_idis the key, and the handler only processes a check whose status is not yet final. - Articles: the same article is returned by many checks and queries. Normalize its URL (lowercase host, drop
www., drop fragments and tracking parameters such asutm_*, strip a trailing slash) and hash it. Syndicated copies on different domains stay separate articles; group them by title similarity if you report stories rather than articles.
Change detection
New coverage is the set of articles whose first_seen falls inside the last check. Alert on:
- Any new article from a tier-1 publisher for a warm or hot entity.
- Velocity above the entity’s baseline, for example new articles in the last 3 hours above the 95th percentile of its trailing 30 days.
- A negative keyword in new titles (recall, lawsuit, breach, layoffs), from a list you maintain.
End-to-end Python
Build a check slot
import datetime as dt
HOOK = {"url": "https://hooks.example.com/news"}
PRIORITY = {"hot": 10, "warm": 5, "background": 1}
def slot_tasks(db, tiers):
slot = dt.datetime.now(dt.timezone.utc).replace(minute=0, second=0, microsecond=0)
rows = db.execute("select query_id, q, country, hl, pages, tier from queries where tier = any(%s)",
(list(tiers),)).fetchall()
tasks = []
for query_id, q, country, hl, pages, tier in rows:
payload = {"query": q, "country": country, "pages": pages}
if hl:
payload["hl"] = hl
tasks.append({"taskType": "GOOGLE_NEWS", "payload": payload, "priority": PRIORITY[tier],
"idempotencyKey": f"news:{query_id}:{country}:{hl}:{slot:%Y-%m-%dT%H}", "webhook": HOOK})
return slot, tasks
Submit in chunks of 500 and insert a checks row per accepted task. Call it from cron: hourly with {"hot"}, every 4 hours with {"hot", "warm"}, daily with all three. The slot-based key stops the hourly and 4-hourly calls from double-submitting the hot tier in the same hour; the second returns RESOURCE_ALREADY_EXISTS. Keep query ids free of :.
Parse dates and URLs
import hashlib, re
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
UNITS = {"min": 60, "minute": 60, "hour": 3600, "day": 86400, "week": 604800, "month": 2592000}
def estimate_published(raw: str | None, captured: dt.datetime):
if not raw:
return None
m = re.match(r"(\d+)\s*(min|minute|hour|day|week|month)s?\s+ago", raw.strip(), re.I)
if m:
return captured - dt.timedelta(seconds=int(m.group(1)) * UNITS[m.group(2).lower()])
for fmt in ("%b %d, %Y", "%d %b %Y", "%Y-%m-%d"):
try:
return dt.datetime.strptime(raw.strip(), fmt).replace(tzinfo=dt.timezone.utc)
except ValueError:
pass
return None # other languages and formats: keep raw_date, estimate later
def url_key(url: str):
p = urlsplit(url)
query = urlencode([(k, v) for k, v in parse_qsl(p.query) if not k.lower().startswith("utm_")])
clean = urlunsplit((p.scheme.lower(), (p.hostname or "").removeprefix("www."), p.path.rstrip("/"), query, ""))
return clean, hashlib.sha256(clean.encode()).hexdigest()
Relative dates in other interface languages (“il y a 2 heures”) need their own patterns; add them per hl you monitor.
Deduplicate and alert
apply runs on each verified, non-test delivery:
TIER1 = {"Reuters", "Financial Times", "Le Monde"}
NEGATIVE = re.compile(r"\b(recall|lawsuit|breach|layoffs?|investigation|fine[ds]?)\b", re.I)
def apply(db, d):
task = d["task"]
captured = dt.datetime.now(dt.timezone.utc)
with db.transaction():
row = db.execute(
"update checks c set status = %s, captured_at = %s from queries q "
"where c.task_id = %s and c.status not in ('COMPLETED','FAILED') and q.query_id = c.query_id "
"returning q.entity_id", (task["status"], captured, task["id"])).fetchone()
if not row or task["status"] != "COMPLETED":
return
entity_id = row[0]
for item in d["response"]["result"].get("newsResults", []):
clean, key = url_key(item["link"])
new = db.execute(
"insert into articles values (%s,%s,%s,%s,%s,%s,%s,%s) on conflict do nothing returning url_key",
(key, clean, item.get("title", ""), item.get("source"), item.get("snippet"), captured,
estimate_published(item.get("date"), captured), item.get("date"))).fetchone()
db.execute("insert into sightings values (%s,%s,%s,%s,%s) on conflict do nothing",
(task["id"], key, entity_id, item.get("page"), item.get("position")))
if new and (item.get("source") in TIER1 or NEGATIVE.search(item.get("title", ""))):
alert(entity_id, item)
def alert(entity_id, item):
print(f"[{entity_id}] {item.get('source')}: {item.get('title')} {item['link']}") # send to your queue
Keep alert fast: push to a queue and return. The endpoint must answer 2xx within 15 seconds or the delivery is retried.
Compute velocity and share of voice
-- New articles per entity per hour, with a 30-day hourly baseline
with hourly as (
select s.entity_id, date_trunc('hour', a.first_seen) as hour, count(distinct a.url_key) as new_articles
from articles a
join sightings s using (url_key)
where a.first_seen > now() - interval '30 days'
group by 1, 2
)
select entity_id,
sum(new_articles) filter (where hour > now() - interval '3 hours') as last_3h,
percentile_cont(0.95) within group (order by new_articles) as p95_hourly
from hourly group by entity_id;
-- News share of voice over the last 7 days
select s.entity_id,
count(distinct s.url_key)::numeric / sum(count(distinct s.url_key)) over () as share_of_voice,
count(distinct a.source) as publisher_reach
from sightings s join articles a using (url_key)
where a.first_seen > now() - interval '7 days'
group by s.entity_id;
An article sighted under queries for two entities counts for both, which is what share of voice should do when two entities appear in the same story.
Check what AI answers say
When an entity goes hot, add a daily question on ChatGPT and Gemini (“What happened with Acme Robotics this week?”) and compare the cited sources with your article set:
def ai_coverage(db, result: dict) -> float:
cited = {url_key(s["url"])[1] for s in result.get("sources", [])}
if not cited:
return 0.0
known = db.execute("select count(*) from articles where url_key = any(%s)", (list(cited),)).fetchone()[0]
return known / len(cited)
Store the answer text too. A claim that no article in your set supports is worth a human look.
Cost planning
At the Starter tier, a Google News task costs $0.0002, and extra pages are free; ChatGPT costs $0.0006 and Gemini $0.0006. Failed tasks are not charged. Higher tiers, reached by 30-day spend, cost less. See balance and pricing.
Worked example: a communications team monitors its brand and 4 competitors, 60 queries in total, in 2 markets.
| Workload | Checks | Cost per check | Cost per 30 days |
|---|---|---|---|
| Warm tier: 20 brand queries × 2 markets, every 4 hours | 40 × 6 × 30 = 7,200 | $0.0002 | $1.44 |
| Background: 40 queries × 2 markets, daily | 80 × 30 = 2,400 | $0.0002 | $0.48 |
| Hot tier: 10 queries, hourly for 5 days, pages 2 | 10 × 24 × 5 = 1,200 | $0.0002 | $0.24 |
| AI checks: 5 questions × ChatGPT + Gemini, daily for 10 days | 5 × 10 = 50 each | $0.0006 + $0.0006 | $0.06 |
| Total | $2.22 |
Hot-tier bursts are the variable cost, so budget for a few per month. A new account’s 100 free requests cover 100 one-page checks, enough to try a few queries before topping up. Tiers are on pricing.
Pitfalls
Relative dates. “3 hours ago” is relative to the page at capture time, and coarse for older items. Estimate from captured_at, keep raw_date, and never sort strictly by the estimate across days.
Syndication. Wire stories reappear on many publisher domains. Counting URLs overstates coverage of a single story; cluster by normalized title if you report stories.
Name collisions. Short or generic brand names match unrelated news. Use quoted phrases, exclusions and a review of new publishers per entity.
Polling interval sets detection speed. An hourly check cannot see an article before the next hour. Report time to first sighting with your interval as its floor.
Redirect links. Use link (the destination) for identity; redirectLink is present only when Google served a redirect.
Language-specific parsing. Date strings and negative keywords differ per language. Configure both per hl.
Alerts on backfill. A new query’s first check returns older articles as “new”. Suppress alerts on a query’s first check.
Which engines fit
| Engine | Monitoring use |
|---|---|
| Google News | The coverage feed: articles with publisher, snippet and display date per market and language |
| Google Search | Wider web coverage, peopleAreSaying forum and social threads, and the AI Overview for a story query |
| ChatGPT | What an assistant answers about the story, with sources to compare against your coverage set when ChatGPT decides to search |
| Gemini | The same check on a second assistant, with sources and inline citationPills |
The Google News API post covers the request shape, and brand crisis AI monitoring covers the AI side during an incident. Request fields are on the Google News engine page.
Questions
What fields does a Google News result include?
Each item in result.newsResults has position, title, link, snippet, source (the publisher name), date, page and, when available, thumbnail. Items Google served as redirects also carry redirectLink beside the destination link.
Is the publication date a timestamp?
No. date is the text Google displays, such as 2 hours ago. Convert it relative to the time you captured the result and keep the raw string, because the conversion is approximate.
How often can I check a query?
As often as your budget allows; each check is one task. Hourly or every few hours suits active stories and brands, daily suits background topics. Deduplicate articles by normalized URL so repeated checks only surface new coverage.
Can I monitor news in other countries and languages?
Yes. Set country (or gl) for the result geography and hl for the interface language, for example country FR with hl fr. Treat each market as its own series.
Why also monitor AI answers for a news story?
Assistants answer questions about companies and events using what they retrieve. Running the same question on ChatGPT or Gemini shows whether answers reflect the coverage, and which articles they cite.
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.