Twitter RSS feed for any X account or List
X publishes no RSS feeds, so a Twitter RSS feed has to be built: a script reads an account's newest posts as JSON, about 20 per request, and rewrites them as an RSS 2.0 file your reader polls.
Short answer: Refreshed every 15 minutes, one feed takes about 2,880 requests a month, and a public X List folds up to 5,000 accounts into that same single request. To build it, use Sorsa API: POST /user-tweets returns 20 posts per request from $0.07 per 1,000, and 100 free requests cover your first 2,000 posts.
- Endpoint
POST /user-tweets- Page size
- 20 posts per request
- Price
- from $0.07 per 1,000 posts
#Does X have an RSS feed?
No. X has no RSS feed for profiles, searches or Lists: add /rss to a profile address and x.com answers with its ordinary web page, which we confirmed on November 2, 2026. Every Twitter RSS feed that works today is generated by someone from X data, so the real choice is what you build it on and who keeps it running.
The ready-made routes come with strings attached:
- RSSHub's X route now runs only with the
auth_tokencookie of a logged-in X account or with official X API keys, according to its route source. A cookie ties the feed to a real account and puts that account at risk. - Hosted feed generators sell the feed as a subscription, and their plans decide how many posts each feed keeps and how often it refreshes.
RSS itself is alive. Feedly, Inoreader, NetNewsWire, FreshRSS, Miniflux and n8n's RSS Read node all subscribe to RSS 2.0, so a feed you build plugs into the tools you already use. If you want a ping on your phone or in Slack rather than a feed, see how to get notified when someone tweets.
#How a Twitter RSS feed is built
A Twitter RSS feed takes five steps, and only the first one touches X data. The rest is a short script and a timer.
-
Fetch the newest posts as JSON. One request returns an account's latest 20 posts with text, date, media and author.
-
Keep what you already have. A small state file stops repeats and lets the feed hold more than one page.
-
Map each post to an RSS item. Title, link, date and text go into the elements every reader understands.
-
Write the XML file. Write it to a temporary file first and swap it in, so a reader never fetches half a feed.
-
Refresh on a schedule. Every 15 to 60 minutes suits most feeds.
Only the first step reads X data; the script, the file and the reader are yours.
For the data step, use Sorsa API, a Twitter (X) API and scraper: one POST /user-tweets request returns an account's newest 20 posts as clean JSON for one API key, with no developer account, and it never asks for your X login. The response schema stays the same from release to release, so a feed script you write today keeps working.
Five RSS elements carry everything a reader shows, and each one maps to a field of that response:
| RSS element | Sorsa API field | Why it matters |
|---|---|---|
title | first 80 characters of full_text | Readers list items by title |
link and guid | https://x.com/ + user.username + /status/ + id | A stable guid stops repeats |
pubDate | created_at, ISO 8601 | RSS wants RFC 822 dates |
description | full_text plus entities[].preview images | The post as HTML |
dc:creator | user.display_name | RSS author must be an email |
title- Sorsa API field
- first 80 characters of
full_text - Why it matters
- Readers list items by title
linkandguid- Sorsa API field
https://x.com/+user.username+/status/+id- Why it matters
- A stable guid stops repeats
pubDate- Sorsa API field
created_at, ISO 8601- Why it matters
- RSS wants RFC 822 dates
description- Sorsa API field
full_textplusentities[].previewimages- Why it matters
- The post as HTML
dc:creator- Sorsa API field
user.display_name- Why it matters
- RSS
authormust be an email
#Get an account's posts as JSON
Send the account's handle to POST /user-tweets, and the newest 20 posts come back in one response. Three steps get you from nothing to that first response.
-
Get a key. Sign up and copy your API key from the dashboard. The first 100 requests are free and never expire.
-
Call the endpoint. Send the username in a JSON body with your key in the
ApiKeyheader. The full parameter list is in the user tweets reference (opens in a new tab).Terminal curl -X POST "https://api.sorsa.io/v3/user-tweets" \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"username": "nasa_launch_desk"}'Response200 OK { "tweets": [ { "id": "1985012345678901234", "full_text": "Static fire complete. Launch window opens Thursday.", "created_at": "2026-11-02T14:05:00Z", "lang": "en", "likes_count": 1840, "retweet_count": 312, "reply_count": 95, "view_count": 120400, "is_reply": false, "entities": [ { "type": "photo", "link": "https://t.co/k3x9demo", "preview": "https://pbs.twimg.com/media/demo-static-fire.jpg" } ], "user": { "id": "1700000000000000042", "username": "nasa_launch_desk", "display_name": "Launch Desk" } } ], "next_cursor": "DAABCgABG8a1demoAAA" } -
Choose what the feed shows.
typepicksposts(the default),repliesorretweets, andwith_replies: truemixes replies into posts.next_cursorfetches the next 20 older posts.
Each post also carries the author's full profile, so a feed never needs a second request for names or avatars. A 403 means a missing key or no requests left, a 429 means more than 20 requests in one second, and failed requests are never charged.
#Turn the JSON into RSS with Python
The script below turns that JSON into a valid RSS 2.0 file with Python's standard library alone, so there is nothing to install. It works for an account, a List or a search, and it remembers the last 50 posts between runs.
"""Write an RSS 2.0 feed of an X account, List or search.
python x_rss.py user nasa_launch_desk
python x_rss.py list 1700000000000000001
python x_rss.py search "from:nasa_launch_desk OR #artemis"
"""
import json
import os
import sys
import urllib.parse
import urllib.request
from datetime import datetime
from email.utils import format_datetime
from xml.sax.saxutils import escape
API = "https://api.sorsa.io/v3"
KEY = os.environ.get("SORSA_API_KEY", "YOUR_API_KEY")
KEEP = 50 # posts the feed keeps
MAX_PAGES = 5 # pages one run may read to catch up
def page(kind, target, cursor):
if kind == "list":
query = {"list_id": target}
if cursor:
query["next_cursor"] = cursor
url = API + "/list-tweets?" + urllib.parse.urlencode(query)
req = urllib.request.Request(url)
else:
if kind == "user":
path, body = "/user-tweets", {"username": target}
else:
path, body = "/search-tweets", {"query": target,
"order": "latest"}
if cursor:
body["next_cursor"] = cursor
req = urllib.request.Request(
API + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
req.add_header("ApiKey", KEY)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.load(resp)
def fetch_new(kind, target, seen):
new, cursor = [], None
for _ in range(MAX_PAGES):
data = page(kind, target, cursor)
tweets = data.get("tweets") or []
new += [t for t in tweets if t["id"] not in seen]
cursor = data.get("next_cursor")
caught_up = any(t["id"] in seen for t in tweets)
if not cursor or not seen or caught_up:
break
return new
def to_item(t):
user = t.get("user") or {}
name = user.get("username", "")
link = f"https://x.com/{name}/status/{t['id']}"
text = " ".join(t.get("full_text", "").split())
when = datetime.fromisoformat(t["created_at"].replace("Z", "+00:00"))
pics = "".join(f'<p><img src="{escape(e["preview"])}"></p>'
for e in t.get("entities") or []
if e.get("type") == "photo" and e.get("preview"))
author = escape(user.get("display_name") or name)
xml = ("<item>"
f"<title>{escape(text[:80])}</title>"
f"<link>{link}</link>"
f'<guid isPermaLink="true">{link}</guid>'
f"<pubDate>{format_datetime(when)}</pubDate>"
f"<dc:creator>{author}</dc:creator>"
f"<description>{escape(escape(text) + pics)}</description>"
"</item>")
return {"id": t["id"], "date": when.isoformat(), "xml": xml}
def main(kind, target):
safe = (c if c.isalnum() or c in "_-" else "-" for c in target)
name = "".join(safe)[:60]
state, feed = name + ".json", name + ".xml"
items = []
if os.path.exists(state):
with open(state, encoding="utf-8") as f:
items = json.load(f)
seen = {i["id"] for i in items}
items += [to_item(t) for t in fetch_new(kind, target, seen)]
items = sorted(items, key=lambda i: i["date"], reverse=True)[:KEEP]
with open(state, "w", encoding="utf-8") as f:
json.dump(items, f)
home = {"user": f"https://x.com/{target}",
"list": f"https://x.com/i/lists/{target}"}
title = escape(f"X {kind}: {target}")
xml = ('<?xml version="1.0" encoding="UTF-8"?>'
'<rss version="2.0" '
'xmlns:dc="http://purl.org/dc/elements/1.1/"><channel>'
f"<title>{title}</title>"
f"<link>{home.get(kind, 'https://x.com/')}</link>"
f"<description>{title}, built with Sorsa API</description>"
+ "".join(i["xml"] for i in items) + "</channel></rss>")
with open(feed + ".tmp", "w", encoding="utf-8") as f:
f.write(xml)
os.replace(feed + ".tmp", feed)
print(f"{feed}: {len(items)} posts")
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
Three details make this feed behave better than a plain conversion of one page:
- It never drops a burst. When an account posts more than 20 times between runs, the script follows
next_cursorback until it meets a post it already has, up to 5 pages. - It holds 50 posts, not 20. The JSON state file keeps earlier posts, so a quiet day doesn't empty the feed.
- It passes a feed validator's usual checks. Dates are RFC 822, every item has a permanent
guid, the author sits indc:creator, and the post's HTML is escaped once for XML.
Run python x_rss.py user nasa_launch_desk and the file nasa_launch_desk.xml appears next to the script. The first run costs one request; each later run costs one request plus one per extra page it needs to catch up.
#One feed for a whole List or a search
A public X List turns up to 5,000 accounts into one feed that costs the same as a feed for one account. With Sorsa API, GET /list-tweets returns the newest posts from every member of the List in one chronological page, so the feed spends one request per refresh no matter how many accounts it follows.
Following 200 accounts as separate feeds every 15 minutes takes 576K requests a month; the same 200 accounts in one List take 2,880. To set one up, make a public List in the X app (or use someone else's public List), copy the number at the end of its address (x.com/i/lists/<number>) and pass it as list_id. The List tweets reference (opens in a new tab) has the parameters, and the Twitter List API guide covers members and followers too.
python x_rss.py list 1700000000000000001
A search feed works the same way through POST /search-tweets with order set to latest. Use it for a handful of accounts without making a List, or for a topic:
from:nasa_launch_desk OR from:orbit_news_digest
#artemis -filter:replies
"launch window" lang:en min_faves:50
Every X search operator works here, back to 2006; the Twitter search operators guide lists them with examples, and the search tweets reference (opens in a new tab) has the request format. A busy List or hashtag can post more than 20 times between refreshes: the script then reads extra pages, one request each, so refresh busy feeds more often rather than less.
#Run it on a schedule and subscribe
A feed stays fresh when the script runs on a timer and a web server hands the file to your reader. On any Linux server, two lines in your crontab hold the key and refresh the feed every 15 minutes:
SORSA_API_KEY=YOUR_API_KEY
*/15 * * * * cd /srv/feeds && python3 x_rss.py user nasa_launch_desk
Then serve the folder. Any web server that already runs on the machine (nginx, Caddy, Apache) can publish /srv/feeds; a self-hosted reader such as FreshRSS or Miniflux on the same server can read the file from a local address. For a quick test, python3 -m http.server 8080 --directory /srv/feeds serves it at http://localhost:8080/nasa_launch_desk.xml.
Paste the feed's address into your reader's "Add feed" box. Set the reader to refresh no more often than the cron runs: a reader that checks every 5 minutes sees the same file three times out of four.
Good to know
A feed URL is for your own reader or your team's tools. Keep it behind a long random path or your reader's login rather than on a public page: the posts in it belong to their authors, and the feed carries public posts only.
#Serve the feed from n8n instead
n8n can serve the same feed at its own URL with four nodes and no server of your own. Each time your reader fetches that URL, the workflow calls Sorsa API once and answers with fresh XML.
-
Add a Webhook node. Set HTTP Method to GET, give it a long random path, and set Respond to "Using 'Respond to Webhook' Node".
-
Add an HTTP Request node. POST to
https://api.sorsa.io/v3/user-tweetswith the headerApiKey, and send the JSON body{"username": "nasa_launch_desk"}. -
Add a Code node. It turns the
tweetsarray into RSS items; the code is in the workflow below. -
Add a Respond to Webhook node. Set Respond With to Text, drag the Code node's
xmlfield into Response Body, and add the response headerContent-Typewithapplication/rss+xml; charset=utf-8. -
Activate the workflow. Subscribe your reader to the Webhook node's Production URL.
n8n workflow: four nodes, one feed URL- WebhookFeed URLGET, responds through the last node
- HTTP RequestSorsa API user tweetsPOST /user-tweets, header ApiKey
- CodeBuild RSStweets to RSS 2.0 items
- Respond to WebhookReturn the feedText, application/rss+xml
const tweets = $input.first().json.tweets || []; const esc = (s) => String(s).replace(/&/g, '&') .replace(/</g, '<').replace(/>/g, '>'); const items = tweets.map((t) => { const link = 'https://x.com/' + t.user.username + '/status/' + t.id; const text = t.full_text.replace(/\s+/g, ' '); return '<item><title>' + esc(text.slice(0, 80)) + '</title>' + '<link>' + link + '</link>' + '<guid isPermaLink="true">' + link + '</guid>' + '<pubDate>' + new Date(t.created_at).toUTCString() + '</pubDate>' + '<description>' + esc(esc(text)) + '</description></item>'; }).join(''); const xml = '<?xml version="1.0" encoding="UTF-8"?>' + '<rss version="2.0"><channel><title>X feed</title>' + '<link>https://x.com/</link>' + '<description>Built with Sorsa API</description>' + items + '</channel></rss>'; return [{ json: { xml } }];{ "name": "X account to RSS feed (Sorsa API)", "nodes": [ { "parameters": { "httpMethod": "GET", "path": "x-feed-3f9c1a7e5b2d", "responseMode": "responseNode", "options": {} }, "name": "Feed URL", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ 0, 0 ], "webhookId": "3f9c1a7e-5b2d-4c8e-9a61-7d0e2b4f6c13" }, { "parameters": { "method": "POST", "url": "https://api.sorsa.io/v3/user-tweets", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "ApiKey", "value": "YOUR_API_KEY" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "{\"username\": \"nasa_launch_desk\"}", "options": {} }, "name": "Sorsa API user tweets", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 220, 0 ] }, { "parameters": { "jsCode": "const tweets = $input.first().json.tweets || [];\nconst esc = (s) => String(s).replace(/&/g, '&')\n .replace(/</g, '<').replace(/>/g, '>');\nconst items = tweets.map((t) => {\n const link = 'https://x.com/' + t.user.username + '/status/' + t.id;\n const text = t.full_text.replace(/\\s+/g, ' ');\n return '<item><title>' + esc(text.slice(0, 80)) + '</title>' +\n '<link>' + link + '</link>' +\n '<guid isPermaLink=\"true\">' + link + '</guid>' +\n '<pubDate>' + new Date(t.created_at).toUTCString() + '</pubDate>' +\n '<description>' + esc(esc(text)) + '</description></item>';\n}).join('');\nconst xml = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' +\n '<rss version=\"2.0\"><channel><title>X feed</title>' +\n '<link>https://x.com/</link>' +\n '<description>Built with Sorsa API</description>' +\n items + '</channel></rss>';\nreturn [{ json: { xml } }];" }, "name": "Build RSS", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 440, 0 ] }, { "parameters": { "respondWith": "text", "responseBody": "={{ $json.xml }}", "options": { "responseHeaders": { "entries": [ { "name": "Content-Type", "value": "application/rss+xml; charset=utf-8" } ] } } }, "name": "Return the feed", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.1, "position": [ 660, 0 ] } ], "connections": { "Feed URL": { "main": [ [ { "node": "Sorsa API user tweets", "type": "main", "index": 0 } ] ] }, "Sorsa API user tweets": { "main": [ [ { "node": "Build RSS", "type": "main", "index": 0 } ] ] }, "Build RSS": { "main": [ [ { "node": "Return the feed", "type": "main", "index": 0 } ] ] } } }In n8n, paste the JSON on an empty canvas, put your key in the HTTP Request node, activate the workflow and subscribe to the Production URL.
The Code node turns Sorsa API's tweets array into RSS 2.0; the workflow JSON brings all four nodes in at once.
Here the reader's refresh interval sets the cost: a reader that checks hourly spends 720 requests a month on the feed. For list or search feeds, change the HTTP Request node's URL and body. If the posts only feed the rest of a workflow (Slack, Sheets, an AI step), skip RSS and read the JSON directly, as the Twitter API in n8n guide shows.
#What a Twitter RSS feed costs
A Twitter RSS feed costs one request per refresh, so its price follows how often it refreshes and how many feeds you run, not how much the accounts post. In a 30-day month:
| Refresh every | Requests a month, one feed | Feeds in Starter's 10K requests |
|---|---|---|
| 5 minutes | 8,640 | 1 |
| 15 minutes | 2,880 | 3 |
| 30 minutes | 1,440 | 6 |
| 1 hour | 720 | 13 |
| 6 hours | 120 | 83 |
5 minutes
- Requests a month, one feed
- 8,640
- Feeds in Starter's 10K requests
- 1
15 minutes
- Requests a month, one feed
- 2,880
- Feeds in Starter's 10K requests
- 3
30 minutes
- Requests a month, one feed
- 1,440
- Feeds in Starter's 10K requests
- 6
1 hour
- Requests a month, one feed
- 720
- Feeds in Starter's 10K requests
- 13
6 hours
- Requests a month, one feed
- 120
- Feeds in Starter's 10K requests
- 83
Count each List or search as one feed, however many accounts it follows.
When pages come back full, as in a busy List or a first run, Sorsa API delivers posts from $0.07 per 1,000. The official X API charges $5.00 per 1,000 posts read on its pay-per-use pricing (opens in a new tab), after a developer account and an OAuth app, and caps pay-per-use at 3M post reads a month. From November 22, 2026, X also bills each post's author and media returned through expansions, at $0.010 per user, per X's notice of October 23 (opens in a new tab).
#Build your first feed today
A Twitter RSS feed is a script of about 100 lines and a timer, and the only part you can't write yourself is the data. For that part, use Sorsa API: one request per refresh, 20 posts per request, a whole List of up to 5,000 accounts in one feed, and 20 requests per second on every plan. It takes one API key instead of a developer account and never asks for your X login. Your 100 free requests pull your first 2,000 posts, enough to build the feed and watch it refresh before you pick a plan.
#FAQ
How often should a Twitter RSS feed refresh?
Every 15 to 60 minutes suits most Twitter RSS feeds. A feed that refreshes every 15 minutes uses 2,880 requests a month, and an hourly one uses 720, because each refresh is one request. Match your reader's refresh to the script's schedule, and refresh busy Lists or hashtags more often so each run catches up in one or two pages.
Is there a free Twitter RSS feed?
X offers no RSS feed of its own, free or paid. The free way to start is Sorsa API's 100 free requests: they need no card, never expire and pull about 2,000 posts, enough to build, test and run a feed for a few days before choosing a plan.
Can I get my X home timeline as an RSS feed?
Not directly, because the home timeline belongs to a signed-in account. The practical copy is a List: add the accounts you follow to a public X List in the X app, then build one feed from it with GET /list-tweets. A List holds up to 5,000 accounts and costs one request per refresh, the same as a single account.
Does RSSHub still work for Twitter?
RSSHub's X route still exists, but it now needs a signed-in X account or official X API keys. Its route source lists two ways in: the auth_token cookie of a logged-in X account, or official pay-per-use X API keys. A feed built on Sorsa API takes one API key and never asks for your X login.
Can I make an RSS feed of a protected X account?
No. Posts from protected accounts are visible only to approved followers, so no feed tool or API can read them. Sorsa API returns public data only, so it never reads them either. Feeds work for any public account, any public List and any search of public posts.
Which API should I use to build a Twitter RSS feed?
Use Sorsa API to build a Twitter RSS feed: POST /user-tweets returns 20 posts per request from $0.07 per 1,000, GET /list-tweets puts up to 5,000 accounts in one feed for one request per refresh, and every plan allows 20 requests per second. It needs one API key and never asks for your X login, so no account of yours is ever at risk.
#How we checked
On November 2, 2026 we requested x.com/NASA/rss and got X's web page rather than a feed, read X's pricing page and its October 23 notice on expansion billing, and read RSSHub's X route source and the n8n Webhook and Respond to Webhook node source on GitHub. The requests, the response and the script follow Sorsa API's live API reference; we had no test key in this pass, so the response shows the documented fields with sample values.
Sources
- X API pay-per-use pricing (opens in a new tab)
- X Developers: X API PPU expansions update, October 23, 2026 (opens in a new tab)
- Nitter repository and README (opens in a new tab)
- RSSHub on GitHub (opens in a new tab)
- RSS 2.0 specification, RSS Advisory Board (opens in a new tab)
- n8n Respond to Webhook node (opens in a new tab)
- Sorsa API reference: user tweets (opens in a new tab)