September 28, 2026 · GNews Team
Social Media API News: Auto-Post Fresh Articles with GNews and Mastodon
If you are looking for social media API news, keep in mind that social media APIs give you posts, not news articles. To run a social account that shares news automatically, you need two APIs: a news API to find fresh articles and the social platform's API to publish them. This tutorial builds that pipeline with the GNews API (search endpoint) and Mastodon's statuses API, in Python, JavaScript, and cURL, and shows how to adapt each post to Bluesky or X character limits. As a running example, the bot shares news about social media itself: platform policy, regulation, and product launches.
Social media API news: why social APIs don't give you news articles
The query "social media API news" hides three different questions:
- Can I get news through a social media API? You get posts, including those of news outlets' accounts, not structured articles.
- How do I push news to social media via an API? A news account or bot that posts articles to Mastodon, Bluesky, X, or LinkedIn. This tutorial builds it.
- How do I follow news about social media? Social media news monitoring (policy, bans, regulation, outages). The running example covers this, since the bot's topic is social media itself.
| Source | What it returns | Best for |
|---|---|---|
| Social platform API | User posts and replies | Listening to conversations and reactions |
| News API (GNews) | Published articles with title, description, url, image, publishedAt, source |
Sharing reported news |
GNews indexes published news articles from news publishers. It does not return tweets, Reddit threads, TikTok videos, or any other user-generated social content, and it has no engagement data such as likes, shares, or trending scores. It also does not post anything to a social network. What it gives you is clean, structured article data: a headline, a short description, the article URL, a preview image, the publish time, and the publisher's name. If your project needs the posts themselves (what people are saying about a story), the platform's own API is the right tool. If it needs the story, a news API is. Most news bots use both halves: GNews to decide what to share, the platform API to share it.
What you'll build
A small news bot that runs on a schedule:
- Fetch the newest articles about social media from the GNews
searchendpoint. - Compose each article into a post that fits the target platform's character limit, with source credit, a hashtag, and the link.
- Publish the best one or two to a Mastodon account through its API.
A fourth step makes it safe to run repeatedly without posting the same article twice. The pattern gives you an auto-post news to social media workflow for any platform with a posting API.
Prerequisites
- A free GNews API key from gnews.io.
- A Mastodon account and an access token. Create one under Preferences > Development > New application, with the
write:statusesscope. - Python 3.9+ (standard library only) or Node.js 18+ (built-in
fetch). - Keys stored in environment variables, never in code:
GNEWS_API_KEY,MASTODON_TOKEN, andMASTODON_URLfor your instance.
Step 1: Fetch fresh news with the GNews search endpoint
The parameters are chosen for a social bot:
q="social media"matches the exact phrase.in=titlerequires the phrase in the headline, so the post you publish is visibly on topic.sortby=publishedAtreturns the newest articles first instead of the most relevant.fromlimits results to articles published after a timestamp. Here it is 48 hours ago; in Step 4 it becomes "since the last run".max=5keeps the response small, because you will only post one or two items per run.
Python
import json
import os
from datetime import datetime, timedelta, timezone
from urllib.parse import urlencode
from urllib.request import urlopen
GNEWS_API_KEY = os.environ["GNEWS_API_KEY"]
def fetch_news(query, since, max_articles=5, **extra):
params = {
"q": query,
"in": "title",
"lang": "en",
"sortby": "publishedAt",
"from": since.strftime("%Y-%m-%dT%H:%M:%SZ"),
"max": max_articles,
"apikey": GNEWS_API_KEY,
**extra,
}
url = "https://gnews.io/api/v4/search?" + urlencode(params)
with urlopen(url, timeout=10) as response:
return json.load(response)
since = datetime.now(timezone.utc) - timedelta(hours=48)
data = fetch_news('"social media"', since)
print(data["totalArticles"], "matching articles")
for article in data["articles"]:
print(article["publishedAt"], article["source"]["name"], "|", article["title"])
JavaScript
Save this as fetch_news.mjs (top-level await needs an ES module) and run it with node fetch_news.mjs.
const GNEWS_API_KEY = process.env.GNEWS_API_KEY;
async function fetchNews(query, since, max = 5, extra = {}) {
const params = new URLSearchParams({
q: query,
in: "title",
lang: "en",
sortby: "publishedAt",
from: since.toISOString().slice(0, 19) + "Z",
max: String(max),
apikey: GNEWS_API_KEY,
...extra,
});
const res = await fetch(`https://gnews.io/api/v4/search?${params}`);
if (!res.ok) throw new Error(`GNews error: ${res.status}`);
return res.json();
}
const since = new Date(Date.now() - 48 * 60 * 60 * 1000);
const data = await fetchNews('"social media"', since);
console.log(`${data.totalArticles} matching articles`);
cURL
curl -s "https://gnews.io/api/v4/search?q=%22social%20media%22&in=title&lang=en&sortby=publishedAt&from=2026-09-26T11:16:36Z&max=5&apikey=$GNEWS_API_KEY"
On September 28, 2026, this request returned 5 articles out of 76 matches, the newest published about 20 minutes before the call. See Example response.
Widen or narrow the feed
Not every headline containing "social media" is industry news: our run also matched celebrity stories where someone "takes to social media". For a feed about the platforms themselves, search their names with OR:
data = fetch_news("Mastodon OR Bluesky OR TikTok OR Instagram", since, country="us")
We ran this variant over the same 48-hour window and got 5 articles out of 20 matches, led by an Alabama trial involving TikTok and a new Instagram photo editing feature. Name matches still catch the odd off-topic story (a jazz profile mentioning TikTok), so run your API feed for a day and read what comes back before going live. For a general headlines account, swap to top-headlines?category=technology. The search endpoint docs list every parameter.
Step 2: Turn an article into a social post
A post has a hard character budget. The compose function builds it from title, source.name, description, and url: headline and credit first, description only when there is room, hashtag and link last. Text is cut on a word boundary with an ellipsis.
Python
def truncate(text, limit):
if len(text) <= limit:
return text
cut = text[: limit - 1].rsplit(" ", 1)[0].rstrip(" ,;:.")
return cut + "…"
def compose_post(article, hashtags, limit=500):
credit = f" (via {article['source']['name']})"
tail = "\n\n" + " ".join(hashtags) + "\n" + article["url"]
budget = limit - len(credit) - len(tail)
post = truncate(article["title"], budget) + credit
description = article.get("description") or ""
room = limit - len(post) - len(tail) - 2
if description and room >= 60:
post += "\n\n" + truncate(description, room)
return post + tail
JavaScript
function truncate(text, limit) {
if (text.length <= limit) return text;
const cut = text.slice(0, limit - 1);
return cut.slice(0, cut.lastIndexOf(" ")).replace(/[ ,;:.]+$/, "") + "…";
}
function composePost(article, hashtags, limit = 500) {
const credit = ` (via ${article.source.name})`;
const tail = `\n\n${hashtags.join(" ")}\n${article.url}`;
let post = truncate(article.title, limit - credit.length - tail.length) + credit;
const description = article.description ?? "";
const room = limit - post.length - tail.length - 2;
if (description && room >= 60) post += `\n\n${truncate(description, room)}`;
return post + tail;
}
Run on the first article of our sample with limit=500, this produces a 373-character post with the description included. With limit=280, the description is dropped and the post keeps the headline, credit, hashtag, and link:
Supreme Court Wants Statutory Rules On 18-Year Age Limit For Social Media (via NDTV Profit)
#SocialMedia
https://www.ndtvprofit.com/india/minors-can-t-enter-contracts-supreme-court-wants-statutory-rules-on-18-year-age-limit-for-social-media-12109028
Character limits per platform
| Platform | Default limit | Official reference |
|---|---|---|
| Mastodon | 500 characters by default, configurable per instance | Mastodon instance API |
| Bluesky | 300 graphemes | app.bsky.feed.post lexicon |
| X | 280 characters | X character counting |
The compose function counts every character of the URL, which is the safe choice. News URLs are often long: the link above is 144 characters. On X, every URL is wrapped by t.co and counts as 23 characters, so the full-length count is conservative there and you can budget 23 instead. For Mastodon and Bluesky, check each platform's docs for how links count, and note that Bluesky's limit is in graphemes, which can differ from Python's len() or JavaScript's length (emoji are the usual edge case). On Mastodon, read the instance's configured limit instead of hard-coding 500. Everywhere: pass the limit into compose_post, keep headline and link intact, and let the description flex.
Attribution and hashtags
Always credit source.name and link the original url: the publisher did the reporting. Derive one or two hashtags from your query terms (#SocialMedia, or #TikTok when the name is in the headline) rather than unrelated trending tags.
Step 3: Publish to Mastodon with its API
Mastodon publishes a post with POST /api/v1/statuses, authenticated with your token and a status form field (statuses API docs). You do not upload an image: Mastodon generates a preview card from the OpenGraph tags of the linked page. The optional Idempotency-Key header, set here to the GNews article id, prevents duplicate submissions of the same status; Mastodon stores keys for up to 1 hour.
This step was not executed for this article (it needs a real account), so it uses placeholders: https://mastodon.example and $MASTODON_TOKEN.
Python
from urllib.request import Request
MASTODON_URL = os.environ.get("MASTODON_URL", "https://mastodon.example")
MASTODON_TOKEN = os.environ["MASTODON_TOKEN"]
def post_to_mastodon(text, article_id):
body = urlencode({"status": text, "language": "en"}).encode()
request = Request(
f"{MASTODON_URL}/api/v1/statuses",
data=body,
headers={
"Authorization": f"Bearer {MASTODON_TOKEN}",
"Idempotency-Key": article_id,
},
method="POST",
)
with urlopen(request, timeout=10) as response:
return json.load(response)
JavaScript
const MASTODON_URL = process.env.MASTODON_URL ?? "https://mastodon.example";
async function postToMastodon(text, articleId) {
const res = await fetch(`${MASTODON_URL}/api/v1/statuses`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MASTODON_TOKEN}`,
"Idempotency-Key": articleId,
},
body: new URLSearchParams({ status: text, language: "en" }),
});
if (!res.ok) throw new Error(`Mastodon error: ${res.status}`);
return res.json();
}
cURL
curl -s -X POST "https://mastodon.example/api/v1/statuses" \
-H "Authorization: Bearer $MASTODON_TOKEN" \
-H "Idempotency-Key: 46d3b81c77673774000aea5336052497" \
--data-urlencode $'status=Supreme Court Wants Statutory Rules On 18-Year Age Limit For Social Media (via NDTV Profit)\n\n#SocialMedia\nhttps://www.ndtvprofit.com/india/minors-can-t-enter-contracts-supreme-court-wants-statutory-rules-on-18-year-age-limit-for-social-media-12109028'
Posting to Bluesky or X instead
Fetch and compose stay the same; only the publish call changes.
Bluesky posts are app.bsky.feed.post records. A bare URL in the text does not produce a preview card, so you attach an external embed filled from GNews fields: uri from url, plus title and description. For a thumbnail, upload the GNews image as a blob and reference it in thumb. Links in the text become clickable through facets, and hashtags through the tag feature of the facet lexicon. The record shape:
{
"$type": "app.bsky.feed.post",
"text": "Supreme Court Wants Statutory Rules On 18-Year Age Limit For Social Media (via NDTV Profit)",
"createdAt": "2026-09-28T12:00:00Z",
"embed": {
"$type": "app.bsky.embed.external",
"external": {
"uri": "https://www.ndtvprofit.com/india/minors-can-t-enter-contracts-supreme-court-wants-statutory-rules-on-18-year-age-limit-for-social-media-12109028",
"title": "Supreme Court Wants Statutory Rules On 18-Year Age Limit For Social Media",
"description": "The Centre has been asked to examine whether requirements for protecting minors can be incorporated into statutory rules."
}
}
}
The Bluesky posts guide covers blob upload, facets, and createRecord. Since the card carries the link, the text can skip the URL.
X uses its own posts endpoint, authentication, and access terms. Check the X API documentation for the current details, then pass limit=280 to the compose function.
Step 4: Post only what's new, at a human pace
A scheduled bot must not repost an article or flood followers. Two guards and one cap handle that:
from= last run time. Save a timestamp at the end of each run and pass it (minus a small overlap) asfromon the next request. GNews then returns only articles published since, sorted newest first bysortby=publishedAt. That gives you near real time news API updates without paging through old results.- A set of posted
idvalues. The overlap window means an article can show up twice, and a failed run can be retried. Before posting, skip any article whose GNewsidyou already stored. - Post the top 1 to 3 per run, not every result, so the account posts steadily instead of in bursts.
from pathlib import Path
STATE_FILE = Path("bot_state.json")
POSTS_PER_RUN = 2
def run_once():
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
now = datetime.now(timezone.utc)
if "last_run" in state:
since = datetime.fromisoformat(state["last_run"]) - timedelta(hours=1)
else:
since = now - timedelta(hours=48)
posted = state.get("posted_ids", [])
data = fetch_news('"social media"', since)
fresh = [a for a in data["articles"] if a["id"] not in posted]
for article in reversed(fresh[:POSTS_PER_RUN]): # oldest first
post_to_mastodon(compose_post(article, ["#SocialMedia"]), article["id"])
posted.append(article["id"])
state = {"last_run": now.isoformat(), "posted_ids": posted[-500:]}
STATE_FILE.write_text(json.dumps(state))
run_once()
Schedule run_once() like any other job (cron, a systemd timer, a scheduled function). Without a persistent disk, keep the state in a database row or key-value store.
Example response
This is the real response to the Step 1 request, trimmed to 2 of the 5 articles. The content field is omitted for brevity; the bot does not use it.
{
"totalArticles": 76,
"articles": [
{
"id": "46d3b81c77673774000aea5336052497",
"title": "Supreme Court Wants Statutory Rules On 18-Year Age Limit For Social Media",
"description": "The Centre has been asked to examine whether requirements for protecting minors can be incorporated into statutory rules.",
"url": "https://www.ndtvprofit.com/india/minors-can-t-enter-contracts-supreme-court-wants-statutory-rules-on-18-year-age-limit-for-social-media-12109028",
"image": "https://c.ndtvimg.com/2025-12/3c94e127_IMG_0315.jpg?im=FeatureCrop,algorithm=dnn,width=1600,height=900",
"publishedAt": "2026-09-28T10:53:55Z",
"lang": "en",
"source": {
"id": "73aabc646bff1356a46223f250cc6b52",
"name": "NDTV Profit",
"url": "https://www.ndtvprofit.com",
"country": "in"
}
},
{
"id": "b357f4a95da76941cb21bfb693490de2",
"title": "ChatGPT plans Sachin Tendulkar's family trip: Cricket legend partners with OpenAI; social media reacts",
"description": "Cricket legend Sachin Tendulkar announced a partnership with OpenAI, highlighting the platform's ability to connect fans through Reddit communities.",
"url": "https://www.livemint.com/ai/chatgpt-plans-sachin-tendulkars-family-trip-cricket-legend-partners-with-openai-social-media-reacts-11790591261436.html",
"image": "https://www.livemint.com/lm-img/img/2026/09/28/1600x900/logo/Sachin_OpenAI_1790591587671_vFTa_1790591599211_ynhO_3a6f0a71-3b98-463f-895b-66d1be64143c_wfeB.jpg",
"publishedAt": "2026-09-28T10:34:24Z",
"lang": "en",
"source": {
"id": "3bd4bd306c42647825fa42989624c036",
"name": "Livemint",
"url": "https://www.livemint.com",
"country": "in"
}
}
]
}
The bot reads only a few fields: totalArticles (total matches, independent of max), id (the idempotency key), title, description, source.name and url (the post itself), image (a Bluesky thumbnail), and publishedAt (logging and the from window).
FAQ
Can I get news from a social media API? A social media API returns posts: what users, brands, and news outlets' own accounts have published on that platform. You get the text of a post and its author, not a structured news article with a headline, description, publisher name, preview image, and publish time. For published articles, use a news API such as GNews, which indexes stories from news publishers and returns them as JSON with those fields. For conversations and reactions around a story, use the platform's own API. The two answer different questions: a news API tells you what was reported, a social API tells you what people are saying. GNews covers the first, not the second: it returns no social posts and no engagement metrics. Many projects combine them, fetching articles from GNews and publishing or discussing them through a social platform's API, which is the pipeline this tutorial builds.
How do I automatically post news to Mastodon, Bluesky, or X? Fetch articles with a news API, compose each within the platform's character limit, and publish through the platform's API on a schedule. Steps 1 to 4 show the loop for Mastodon, with notes for Bluesky and X.
How do I avoid posting the same article twice?
Pass the time of your last run as the from parameter, so GNews only returns newer articles, and keep a stored list of the GNews article id values you have already posted. Skip any article whose id is on the list.
How do I get a link preview image on my posts?
Mastodon generates the preview card from the linked page's OpenGraph tags. Bluesky needs an explicit external embed, which you fill from the GNews url, title, description, and image. Unless you pass nullable=image, the GNews API only returns articles that have an image, which suits link previews; see the search endpoint docs.
Is there a free news API I can use for a social media bot? GNews has a free plan for development; see gnews.io/pricing for what each plan includes.
Next steps
The GNews search endpoint reference lists every parameter used here and the query syntax for narrower feeds. From there, try a separate account per topic or another language with lang.
Ready to build? Start building for free with GNews.