Fetch Twitter/X post content by URL using jina.ai API to bypass JavaScript restrictions...
Fetch Twitter/X post and article content with full media support.
For plain post text, prefer the fxtwitter mirror API โ login-free, key-free,
works direct, and returns the full note_tweet body in tweet.text (the
full_text key does not exist; a 2,324-char long-form announcement came back
complete):
curl -sS --max-time 20 "https://api.fxtwitter.com/<user>/status/<id>" \
| python3 -c "import json,sys; t=json.load(sys.stdin)['tweet']; print(t['created_at']); print(t['text'])"
replies is a count, not the reply thread. For X Articles with images, use
fetch_article.py below โ fxtwitter does not carry article bodies.
uv run --with pyyaml python scripts/fetch_article.py <article_url> [output_dir]
Example:
uv run --with pyyaml python scripts/fetch_article.py \
https://x.com/HiTw93/status/2040047268221608281 \
./Clippings
This will:
twitter-cli (likes, retweets, bookmarks)jina.ai APIattachments/YYYY-MM-DD-AUTHOR-TITLE/Metadata and plain article text come from twitter-cli; the image URLs ride in
the markdown Jina's reader returns. When Jina refuses (see the section below),
the script warns on stderr, keeps the twitter-cli text, and reports Images: 0.
The Markdown is still correct; it simply has no pictures. Images: 0 on an
article that visibly contains images is the signature of a Jina refusal.
Fetching: https://x.com/HiTw93/status/2040047268221608281
--------------------------------------------------
Getting metadata...
Title: ไฝ ไธ็ฅ้็ๅคงๆจกๅ่ฎญ็ป๏ผๅ็ใ่ทฏๅพไธๆฐๅฎ่ทต
Author: Tw93
Likes: 1648
Getting content and images...
Images: 15
Downloading 15 images...
โ 01-image.jpg
โ 02-image.jpg
...
โ Saved: ./Clippings/2026-04-03-ๆ็ซ ๆ ้ข.md
โ Images: ./Clippings/attachments/2026-04-03-HiTw93-.../ (15 downloaded)
Anonymous r.jina.ai access to x.com gets blocked for hours at a time when
third-party callers abuse the domain. The block hits every anonymous caller and
then expires on its own. Both states showed up minutes apart on 2026-09-12.
A refusal does not look like a failure. curl exits 0 and the body is a JSON
envelope:
{"data":null,"code":403,"name":"AbuseAlleviationError","status":40305,
"message":"Anonymous access to domain x.com blocked until <date> ..."}
A lapsed or unfunded key returns the same shape with "code":402, "name":"InsufficientBalanceError". curl --fail catches neither: this endpoint
answered HTTP 200 with the envelope in the body. The one signal that holds is
the Markdown Content: marker that Jina's reader puts in front of the article
body: no marker, no article. fetch_article.py tests for that marker before
accepting a response, which keeps a refusal envelope out of the generated
Markdown.
scripts/fetch_tweets.sh and scripts/fetch_tweet.py both require
JINA_API_KEY and have no second source to fall back to, so they stop working
whenever that key lapses. Both check the same marker and fail loudly on a
refusal โ the Python one raises without writing the output file, the shell one
reports the URL on stderr and exits non-zero โ rather than handing back an
envelope dressed up as a post. No key ships with this repository.
For simple text-only fetching:
# Single tweet
curl "https://r.jina.ai/https://x.com/USER/status/TWEET_ID" \
-H "Authorization: Bearer ${JINA_API_KEY}"
# Batch fetching
scripts/fetch_tweets.sh url1 url2 url3
JINA_API_KEY and have no fallbackuv (Python package manager)export JINA_API_KEY="your_api_key_here"
# Get from https://jina.ai/
output_dir/
โโโ YYYY-MM-DD-article-title.md # Main Markdown file
โโโ attachments/
โโโ YYYY-MM-DD-author-title/
โโโ 01-image.jpg
โโโ 02-image.jpg
โโโ ...
https://x.com/USER/status/ID (posts)https://x.com/USER/article/ID (long-form articles)https://twitter.com/USER/status/ID (legacy)Full-featured article fetcher with image download:
uv run --with pyyaml python scripts/fetch_article.py <url> [output_dir]
Simple text-only fetcher using Jina API:
python scripts/fetch_tweet.py <tweet_url> [output_file]
Batch fetch multiple tweets (Jina API):
scripts/fetch_tweets.sh <url1> <url2> ...
This skill reads metadata through twitter-cli, so the tool's own limits apply.
twitter bookmarks and the other timeline commands stop at 500 items however
the config is written. That ceiling is a module constant in the client:
_ABSOLUTE_MAX_COUNT = 500 in twitter_cli/client.py, applied as
min(maxCount, 500) when the client is constructed. Both lines read from
version 0.8.5. X's own bookmark timeline goes far deeper than that.
Two consequences are easy to get wrong:
maxCount has to sit under the rateLimit: section of the config. Put it
under fetch: and the client ignores it without any message, falling back toGoing deeper means calling the GraphQL endpoint directly: the Bookmarks
operation with the client's own feature flags, parsed by
twitter_cli.parser.parse_timeline_response, paging until the platform stops
returning a cursor or returns the same cursor twice. Stop on the platform's
signal rather than on a count.
Old workflow:
curl "https://r.jina.ai/https://x.com/..."
# Manual image extraction and download
New workflow:
uv run --with pyyaml python scripts/fetch_article.py <url>
# Automatic image download, complete Markdown