AnswerLine Start free

Use cases

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

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"}'

Sync or queue it with POST /v1/async/task. Full field list on the Google News page and in the API reference.

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

  1. Quote exact names: "Acme Robotics" rather than acme robotics. Google’s phrase and exclusion operators apply, so check a sample of results whenever you add a query.
  2. Write one query per variant that matters (legal name, brand name, ticker, product names, executive names) and map each to an entity id.
  3. Exclude known collisions: "Acme" -cartoon. Keep exclusions in data, not code.
  4. Add topic queries for stories without a single entity (“EU battery regulation”), tagged to a topic id.
  5. Set markets with country or gl for geography and hl for language. A French check is country: "FR", hl: "fr", with the query in French where the name is translated.
  6. Choose depth: pages accepts 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:

Change detection

New coverage is the set of articles whose first_seen falls inside the last check. Alert on:

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