July 27, 2026 · GNews Team
Messaging API News: How to Pull Headlines with GNews and Post Them to Slack or Discord
Setting up messaging API news, getting live headlines to post automatically into a chat app, comes down to two pieces you wire together yourself: a news API that returns articles as structured data, and your messaging platform's webhook that turns a POST request into a channel message. This tutorial uses the GNews API to pull technology headlines and posts them to a Slack (or Discord) channel with a plain POST request. By the end you'll have a script that polls for news and delivers formatted messages to a chat channel on a schedule, in Python, JavaScript, and cURL.
What you'll build
A small pipeline with three parts: fetch articles from GNews, format each one into a short chat message, and deliver that message to a Slack or Discord channel through an incoming webhook. A fourth part, a polling loop with deduplication, turns the one-off script into something you can run continuously as a lightweight news bot.
What "messaging api news" actually means
If you searched for a "messaging API for news," what you're really after is two separate pieces that you wire together yourself:
- A news feed API (or real-time news api) that supplies the articles, headlines, sources, and publish timestamps as structured JSON. GNews is that piece here.
- A messaging platform's webhook API (Slack, Discord, Telegram, and similar) that accepts a POST request and turns it into a message in a channel.
No provider ships a service that watches the news and drops messages into your Slack workspace out of the box. GNews supplies the news data through its REST API. The "messaging" half is an integration pattern you build on top of an incoming webhook. That's a good thing: it means you control the format, the filtering, and the schedule instead of being stuck with someone else's bot defaults.
Step 1: Fetch news with the GNews API
GNews exposes two endpoints that cover most "news feed api" use cases: top-headlines for a curated, category-based feed, and search for keyword filtering.
Get headlines with top-headlines (category=technology)
This is the closest match to a default "news bot feed": a ready-made list of current top stories in a category, no query required beyond the category itself.
Python
import os
import requests
API_KEY = os.environ["GNEWS_API_KEY"]
def fetch_top_headlines(category="technology", lang="en", country="us", max_articles=5):
url = "https://gnews.io/api/v4/top-headlines"
params = {
"category": category,
"lang": lang,
"country": country,
"max": max_articles,
"apikey": API_KEY,
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()["articles"]
articles = fetch_top_headlines()
print(f"Fetched {len(articles)} articles")
JavaScript (Node 18+)
const API_KEY = process.env.GNEWS_API_KEY;
async function fetchTopHeadlines({ category = "technology", lang = "en", country = "us", max = 5 } = {}) {
const params = new URLSearchParams({ category, lang, country, max: String(max), apikey: API_KEY });
const res = await fetch(`https://gnews.io/api/v4/top-headlines?${params}`);
if (!res.ok) throw new Error(`GNews error: ${res.status}`);
const data = await res.json();
return data.articles;
}
const articles = await fetchTopHeadlines();
console.log(`Fetched ${articles.length} articles`);
cURL
curl -s "https://gnews.io/api/v4/top-headlines?category=technology&lang=en&country=us&max=5&apikey=$GNEWS_API_KEY"
Filter by topic with search (q=<keyword>)
If your channel needs a narrower feed (a product name, a company, a specific technology), swap to search and pass a free-text q parameter instead of a fixed category. This directly answers "can I filter news by topic before sending it to a channel": yes, that's what this endpoint is for.
Python
def fetch_search(query, lang="en", max_articles=5):
url = "https://gnews.io/api/v4/search"
params = {
"q": query,
"lang": lang,
"max": max_articles,
"apikey": API_KEY,
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()["articles"]
articles = fetch_search("technology")
JavaScript
async function fetchSearch(query, { lang = "en", max = 5 } = {}) {
const params = new URLSearchParams({ q: query, lang, max: String(max), apikey: API_KEY });
const res = await fetch(`https://gnews.io/api/v4/search?${params}`);
if (!res.ok) throw new Error(`GNews error: ${res.status}`);
return (await res.json()).articles;
}
cURL
curl -s "https://gnews.io/api/v4/search?q=technology&lang=en&max=5&apikey=$GNEWS_API_KEY"
Once you're past the first call, docs.gnews.io also documents a sortby parameter (publishedAt or relevance) you can add to search to control ordering, and from / to for date-range filtering. Both are worth knowing before you build a polling loop, since ordering by publish date makes deduplication easier (more on that in Step 4).
Example response
Here's a real, unmodified excerpt from the top-headlines call above, trimmed to two of the five returned articles:
{
"totalArticles": 178400,
"articles": [
{
"id": "ee2b4f0a9ca363d9a2a2e02cf287b5fa",
"title": "Cruis'n Blast Has Been Delisted From The Switch eShop",
"description": "Speeding away - Back in 2021, the Cruis'n series returned to console with the release of the arcade title Cruis'n Blast on the Switch. Now, in a surprising ...",
"url": "https://www.nintendolife.com/news/2026/07/cruisn-blast-has-been-delisted-from-the-switch-eshop",
"image": "https://images.nintendolife.com/1967ffff3b681/large.jpg",
"publishedAt": "2026-07-01T00:30:00Z",
"lang": "en",
"source": {
"id": "dd2cbc293c058fbe38c99af3f90e76c4",
"name": "Nintendo Life",
"url": "https://www.nintendolife.com"
}
},
{
"id": "ad258abfaeb725a308a1d0aa03a5519c",
"title": "Xbox Layoff Plans Reportedly Include Closing Arkane, Canceling Blade",
"description": "The cuts will apparently start next week.",
"url": "https://www.engadget.com/2205451/xbox-layoff-plans-reportedly-include-closing-arkane-canceling-blade/",
"image": "https://www.engadget.com/img/gallery/xbox-layoff-plans-reportedly-include-closing-arkane-canceling-blade/l-intro-1782853048.jpg",
"publishedAt": "2026-06-30T20:59:40Z",
"lang": "en",
"source": {
"id": "48e27292ccc1733d7eefd6a41c5655d4",
"name": "Engadget",
"url": "https://www.engadget.com"
}
}
]
}
totalArticles reports how many results exist in total for that query, independent of the max you requested. Each article carries id, title, description, content (omitted above for brevity), url, image, publishedAt, lang, and a source object. Those are the fields you'll map into a chat message next.
Step 2: Format the response into a chat message
Mapping the JSON article fields to a message string
Slack and Discord both accept plain strings in their webhook payload, so the formatting step is just string building: pick the fields you want (title, source.name, url, publishedAt), and produce one line per article.
Python
from datetime import datetime
def format_article(article):
published = datetime.fromisoformat(article["publishedAt"].replace("Z", "+00:00"))
return f"*{article['title']}*\n{article['source']['name']} - {published:%Y-%m-%d %H:%M UTC}\n{article['url']}"
def format_digest(articles, heading="Technology headlines"):
lines = [f"*{heading}*", ""]
lines += [format_article(a) for a in articles]
return "\n\n".join(lines)
message = format_digest(articles)
JavaScript
function formatArticle(article) {
const published = new Date(article.publishedAt).toISOString().slice(0, 16).replace("T", " ");
return `*${article.title}*\n${article.source.name} - ${published} UTC\n${article.url}`;
}
function formatDigest(articles, heading = "Technology headlines") {
return [`*${heading}*`, "", ...articles.map(formatArticle)].join("\n\n");
}
const message = formatDigest(articles);
Keep the digest short: five articles per message is usually the sweet spot for readability in a chat channel, which is why max=5 is used throughout this tutorial.
Step 3: Deliver it to a messaging platform
Create a Slack (or Discord) incoming webhook
Slack: go to your Slack workspace's app settings, create (or reuse) an app, enable "Incoming Webhooks" under Features, and add a new webhook to the channel you want to post in. Slack gives you a URL that looks like https://hooks.slack.com/services/T000/B000/XXXXXXXX.
Discord: open the target channel's settings, go to Integrations > Webhooks, click "New Webhook," and copy the webhook URL. It looks like https://discord.com/api/webhooks/123456789/abcdefg.
Either way, treat that URL as a secret: anyone with it can post to your channel.
POST the formatted message to the webhook
Slack expects a JSON body with a text key. Discord expects content. Everything else about the request is a standard POST with a JSON payload.
Python (Slack)
import requests
SLACK_WEBHOOK_URL = os.environ["SLACK_WEBHOOK_URL"]
def post_to_slack(text):
response = requests.post(SLACK_WEBHOOK_URL, json={"text": text}, timeout=10)
response.raise_for_status()
post_to_slack(message)
Python (Discord)
DISCORD_WEBHOOK_URL = os.environ["DISCORD_WEBHOOK_URL"]
def post_to_discord(content):
response = requests.post(DISCORD_WEBHOOK_URL, json={"content": content}, timeout=10)
response.raise_for_status()
post_to_discord(message)
JavaScript (Slack)
async function postToSlack(text) {
const res = await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
if (!res.ok) throw new Error(`Slack webhook error: ${res.status}`);
}
await postToSlack(message);
cURL (Slack)
curl -s -X POST -H "Content-Type: application/json" \
-d "{\"text\": \"New tech headlines are in.\"}" \
"$SLACK_WEBHOOK_URL"
cURL (Discord)
curl -s -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"New tech headlines are in.\"}" \
"$DISCORD_WEBHOOK_URL"
Swap text for content (and the URL) and the same pattern works for either platform.
Step 4: Poll on a schedule (real-time-ish delivery)
A news API doesn't push data at you; you pull it on an interval. "Real-time" delivery into a chat channel means polling frequently enough that the delay feels negligible, while staying under your plan's rate limit.
Simple loop / cron, dedup by article URL
Without deduplication, every poll would repost the same headlines until new ones appear. Track which article URLs you've already sent and skip them on the next run.
Python
import time
import json
from pathlib import Path
SEEN_FILE = Path("seen_urls.json")
def load_seen():
if SEEN_FILE.exists():
return set(json.loads(SEEN_FILE.read_text()))
return set()
def save_seen(seen):
SEEN_FILE.write_text(json.dumps(list(seen)))
def poll_and_post(interval_seconds=900):
seen = load_seen()
while True:
articles = fetch_top_headlines()
new_articles = [a for a in articles if a["url"] not in seen]
if new_articles:
post_to_slack(format_digest(new_articles))
seen.update(a["url"] for a in new_articles)
save_seen(seen)
time.sleep(interval_seconds)
# poll_and_post() # run this in a long-lived process, or trigger fetch_top_headlines + post once from a cron job
For a one-shot version instead of a long-running loop, drop the while True and time.sleep, and schedule the script itself with cron (*/15 * * * *) or a serverless scheduled function. Either approach needs the same seen_urls persistence, just backed by a file, a small database row, or a key-value store if you're running on serverless infrastructure without a persistent local disk.
JavaScript (interval version)
import fs from "fs";
const SEEN_FILE = "seen_urls.json";
const seen = new Set(fs.existsSync(SEEN_FILE) ? JSON.parse(fs.readFileSync(SEEN_FILE)) : []);
async function pollAndPost() {
const articles = await fetchTopHeadlines();
const newArticles = articles.filter((a) => !seen.has(a.url));
if (newArticles.length) {
await postToSlack(formatDigest(newArticles));
newArticles.forEach((a) => seen.add(a.url));
fs.writeFileSync(SEEN_FILE, JSON.stringify([...seen]));
}
}
setInterval(pollAndPost, 15 * 60 * 1000);
Fifteen minutes is a reasonable starting interval for a technology news channel. Tighten it if your plan's rate limit allows, or if the topic you're tracking moves fast (breaking news on a specific company, for instance).
FAQ
How do I send news headlines to Slack or Discord automatically? Fetch articles from a news API like GNews, format each one into a short text message, and POST that message to a Slack or Discord incoming webhook URL. Run the fetch-format-post sequence on a schedule (a loop, cron job, or scheduled function) to keep the channel updated without manual work.
Is there a free news API I can use for a bot? Yes. GNews has a free plan with no credit card required, which covers building and testing a channel bot before you need higher volume. Sign up at gnews.io to get a key.
How often can I poll a news API without hitting rate limits? It depends on your plan's daily request allowance. A 15 to 30 minute interval is a common default for a single channel and keeps well under most free-tier limits; check your dashboard for the exact ceiling on your account and scale the interval to match.
Can I filter news by topic or keyword before sending it to a channel?
Yes. Use the search endpoint with a q parameter set to your topic or keyword instead of top-headlines with a fixed category. That's the difference between a general feed and a targeted one.
What's the difference between a news feed API and a real-time news api? In practice the terms describe the same kind of service from different angles: a news feed API emphasizes structured, ongoing article delivery (what GNews returns as JSON), while "real-time news api" emphasizes how fresh that data is. GNews continuously indexes new articles, so polling it on a short interval gives you feed data that's effectively real-time for a chat bot use case.
Next steps
Full endpoint and parameter reference, including sortby, from/to date filtering, and pagination, lives at docs.gnews.io. Combine that with the webhook pattern above and you have a topic-filtered, deduplicated news bot for Slack or Discord in well under a hundred lines of code.
Ready to build? Start building for free with GNews.