Recipes
Patterns that work, with the reasoning behind each choice. All of them run without a key.
Monitor a topic without repeating yourself
The trap is sorting by publication date: publishers backdate, so a newly crawled article
can appear behind your cursor and be missed forever. Sort by crawled,
which only moves forward.
import requests, time
BASE = "https://freenewsapi.ai/v1/search"
seen = set()
def poll(topic, minutes=15):
r = requests.get(BASE, params={
"q": topic, "sort": "crawled", "from": f"now-{minutes}m", "size": 100,
}, timeout=20).json()
fresh = [a for a in r["results"] if a["id"] not in seen]
seen.update(a["id"] for a in fresh)
return fresh
while True:
for a in poll("central bank rate decision"):
print(a["published_at"], a["host"], a["title"])
time.sleep(900) # new batches land hourly; 15 min is plentyDetect breaking news by corroboration
One publisher is a claim. Fifteen publishers within an hour is an event. Count independent hosts rather than articles — wire republication inflates the article count without adding a single source.
def is_breaking(topic, min_publishers=8):
r = requests.get("https://freenewsapi.ai/v1/stats", params={
"q": topic, "date": "24h", "top": 100,
}, timeout=30).json()
publishers = len(r["hosts"])
today = list(r["by_day"].values())[0] if r["by_day"] else 0
return publishers >= min_publishers and today > 20, publishers, todayMorning digest, deduplicated
from collections import defaultdict
def digest(country="US", lang="en", top=10):
r = requests.get("https://freenewsapi.ai/v1/search", params={
"country": country, "lang": lang, "date": "24h",
"sort": "date", "size": 100, "strict_country": "true",
}, timeout=20).json()
# group by the first six significant words of the headline
groups = defaultdict(list)
for a in r["results"]:
key = " ".join(w.lower() for w in a["title"].split() if len(w) > 3)[:60]
groups[key].append(a)
ranked = sorted(groups.values(), key=len, reverse=True)[:top]
for g in ranked:
print(f"{len(g)} sources | {g[0]['title']}")
for a in g[:3]:
print(f" {a['host']} {a['url']}")Note strict_country=true: without it a US digest fills with Indian
publishers whose templates declare en-US.
Compare how countries cover the same event
The most interesting question a news API can answer, and one nobody makes easy.
def by_country(topic, countries=("US", "GB", "DE", "FR", "IN", "UA", "BR")):
for cc in countries:
r = requests.get("https://freenewsapi.ai/v1/search", params={
"q": topic, "country": cc, "date": "7d",
"sort": "relevance", "size": 5, "strict_country": "true",
}, timeout=20).json()
print(f"\n=== {cc}: {r['total']} articles")
for a in r["results"]:
print(f" {a['host']:<28} {a['title'][:70]}")Feed the result to a model and ask what differs in framing between the slices. The answer is usually more interesting than the articles.
Track a story from its first appearance
def timeline(topic):
r = requests.get("https://freenewsapi.ai/v1/stats",
params={"q": topic, "date": "30d"}, timeout=30).json()
for day, n in sorted(r["by_day"].items()):
print(f"{day} {'#' * min(n // 5, 60)} {n}")
first = requests.get("https://freenewsapi.ai/v1/search", params={
"q": topic, "date": "30d", "sort": "date_asc", "size": 3,
}, timeout=20).json()
print("\nEarliest coverage:")
for a in first["results"]:
print(f" {a['published_at']} {a['host']} {a['url']}")Watch one publisher
curl "https://freenewsapi.ai/v1/search?host=www.reuters.com&sort=crawled&size=50"Find the exact hostname first — publishers differ on the www.
prefix, and host matches literally:
curl "https://freenewsapi.ai/v1/stats?q=reuters&top=20" | jq .hostsBuild a training or evaluation set
Walk hour by hour, keep the bodies, respect the throttle. The full loop is in
pagination. Two rules: sort by date_asc so the
window is stable while you page it, and use id for deduplication — it
is the MD5 of the URL and never changes.
Planning a full-corpus export? Tell us first. There is a better way to hand you 100,000 articles than 100,000 HTTP requests, and we would rather give you the better way.