X (Twitter) Bulk Tweet Extractor avatar

X (Twitter) Bulk Tweet Extractor

Pricing

from $5.00 / 1,000 tweet extracteds

Go to Apify Store
X (Twitter) Bulk Tweet Extractor

X (Twitter) Bulk Tweet Extractor

Extract complete X (Twitter) post records from a list of tweet URLs or IDs. Returns post text, author, likes, replies, hashtags, mentions, links, the parent of a reply, and direct photo and video file URLs. No login, no API key. Deleted tweets are reported, never charged.

Pricing

from $5.00 / 1,000 tweet extracteds

Rating

0.0

(0)

Developer

angel nguyen

angel nguyen

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

X (Twitter) Bulk Tweet Extractor collects complete post records from a list of tweet URLs or IDs you already have. You paste the links, you get one typed row per post — text, author, likes, replies, hashtags, mentions, outbound links, the post a reply was replying to, and direct file URLs for every photo and video. There is no login, no API key and no developer account anywhere in it.

It is built for the job that starts with a list: a spreadsheet of links from a report, the citations in an article, the posts an alerting tool flagged overnight. If you already know which posts you care about, this turns them into data.

X (Twitter) Bulk Tweet Extractor

Key features

  • Paste anything that identifies a post. Full x.com or twitter.com status URLs, i/web/status links, mobile links, or bare numeric IDs — mixed together in one list.
  • Duplicates are removed before anything is fetched, so the same post pasted twice is fetched once and charged once.
  • 32 fields per post, including the ones that are easy to lose: text of posts past the 280-character display limit (326 characters returned whole in testing), the parent post of a reply, and per-file media details.
  • Deleted posts are reported, not silently dropped. A post that is gone, protected or suspended comes back as a row with available: false and a reason — and is never charged.
  • Direct media file URLs. For video, the highest-bitrate MP4 is chosen; an HLS-only stream is flagged rather than passed off as a downloadable file.
  • No account, no cookies, no rate-limit dance. The Actor reads X's own public post endpoint.

How it works

  1. You give it a list of tweet URLs or IDs in Tweet URLs or IDs.
  2. Every entry is parsed to a numeric post ID. Anything that is not a post reference is reported in the run log and skipped — it is never guessed at.
  3. The list is de-duplicated, then capped at Maximum tweets per run so a large paste cannot cost more than you intended.
  4. Each post is fetched from X's public endpoint, paced by Delay between requests.
  5. Each result is sorted into one of three states — available, gone, or never existed — and written to the dataset.

The three-state handling is the part worth reading twice. A scraper that treats "deleted" and "failed" as the same thing produces a dataset you cannot trust, because a missing row could mean either. Here they are separate values in unavailableReason, and neither of them is charged.

Input

Every field below is optional except the list itself.

FieldTypeDefaultWhat it does
tweetUrlsarrayrequiredThe tweets to extract, one per line. Accepts full x.com or twitter.com status URLs, i/web/status links, and bare numeric tweet IDs — you can mix all three. Duplicates are removed before anything is fetched, so a list pasted twice is never charged twice.
maxTweetsinteger1000A hard ceiling on how many tweets this run will fetch, applied after duplicates are removed. Use it to bound the cost of a large paste. Set it to the number of tweets you are willing to pay for.
includeMediabooleantrueReturn direct file URLs for every photo and video attached to a tweet, with width, height, duration and bitrate. The highest-bitrate MP4 is chosen for video. Turn this off to extract text and metrics only, and the media charge never applies.
includeUserProfilebooleantrueAdd the author block to each record: display name, @handle, numeric account ID, profile image URL, and the legacy verified and Blue verified flags.
includeParentTweetbooleantrueWhen a tweet is a reply, attach the post it replies to as a nested record with its text, author, likes, retweets and reply count. It lets you read a reply in context without a second lookup.
includeUnavailablebooleantrueEmit a row for every requested tweet that could not be read, carrying available=false and the reason — deleted, protected, suspended, or an ID that never existed. These rows are never charged. Turn this off to receive only successful extractions.
languagestring"en"The language X uses for the strings it localises, such as the explanation returned on an unavailable tweet. It does not translate tweet text.
requestDelayMsinteger250Milliseconds to wait between tweets. The default of 250 ms paces a run politely against the source. Raise it for very large lists; 0 disables the pause.

Example input — this is the Actor's own prefill, so it is exactly what you see in the form:

{
"tweetUrls": [
"https://x.com/jack/status/20",
"https://x.com/Interior/status/463440424141459456",
"1349129669258448897"
],
"maxTweets": 100,
"includeMedia": true,
"includeUserProfile": true,
"includeParentTweet": true,
"includeUnavailable": true,
"language": "en",
"requestDelayMs": 250
}

Output

One row per requested post. This is a real row from a real run of this Actor, not an illustration:

{
"tweetId": "2049057344013881523",
"url": "https://x.com/MushtaqBilalPhD/status/2049057344013881523",
"inputRef": "2049057344013881523",
"available": true,
"unavailableReason": null,
"text": "> be Alexandra Elbakyan\n> be born in Kazakhstan in 1988\n> start coding at 12\n> hack your internet provider at 14\n> hack MIT Press at 16 to download neuroscience books you can't afford\n> get a CS degree from Satbayev University\n> intern in neuroscience at Georgia Tech\n> speak at https://t.co/xt74OVA2kW",
"textLength": 326,
"isLongForm": true,
"lang": "en",
"createdAt": "2026-04-28T09:25:15.000Z",
"favoriteCount": 34176,
"replyCount": 241,
"isEdited": false,
"possiblySensitive": false,
"isReply": false,
"inReplyToStatusId": null,
"inReplyToScreenName": null,
"hashtags": [],
"mentions": [],
"links": [],
"hasMedia": true,
"mediaCount": 1,
"media": [
{
"type": "photo",
"previewImageUrl": "https://pbs.twimg.com/media/HG-2y7ybIAA54vq.jpg",
"width": 996,
"height": 560,
"altText": "Alexandra Elbakyan, the founder of Sci-Hub",
"tweetPermalink": "https://x.com/MushtaqBilalPhD/status/2049057344013881523/photo/1",
"fileUrl": "https://pbs.twimg.com/media/HG-2y7ybIAA54vq.jpg",
"format": "jpg",
"durationMs": null,
"bitrate": null,
"isPlaylist": false
}
],
"placeName": null,
"parentTweet": null,
"authorId": "1513235298192437262",
"authorName": "Mushtaq Bilal, PhD",
"authorScreenName": "MushtaqBilalPhD",
"authorUrl": "https://x.com/MushtaqBilalPhD",
"authorVerified": false,
"authorBlueVerified": true,
"authorProfileImageUrl": "https://pbs.twimg.com/profile_images/2047218284727066624/e4pD5FkY_normal.jpg"
}

And this is what a post that is no longer available looks like. It carries the reason, it carries no invented fields, and it was not charged:

{
"tweetId": "1445078208190291973",
"url": "https://x.com/i/status/1445078208190291973",
"inputRef": "https://x.com/i/web/status/1445078208190291973",
"available": false,
"unavailableReason": "TOMBSTONE",
"unavailableDetail": "This Post is unavailable. Learn more",
"text": null,
"textLength": null,
"hasMedia": false,
"mediaCount": 0,
"media": [],
"parentTweet": null
}

The three availability states

unavailableReasonWhat it meansCharged
(null, available: true)The post was read and returned in fullYes
TOMBSTONEThe post existed and is gone — deleted, protected, suspended or age-restrictedNo
NOT_FOUNDNo post has ever had this IDNo

Set Report unavailable tweets to false if you want only the successful rows.

Pricing

This Actor bills per delivered unit. A post it could not read costs nothing, and a run that reads nothing costs nothing.

EventPriceWhen you are charged
tweet-extracted$0.005Charged once per tweet record delivered to the dataset. A tweet that is deleted, protected, suspended, or an ID that never existed is reported with available=false and is never charged, and duplicate URLs in your input are removed before anything is fetched.
media-file-resolved$0.0015Charged once per photo or video file whose direct URL, dimensions and bitrate were resolved and returned. It applies only when Resolve media files is on and the tweet actually carries media, so a text-only tweet costs the extraction charge alone.

What that works out to:

RunExtractionMediaTotal
100 text-only posts$0.50$0.00$0.50
100 posts, 40 of them with one image$0.50$0.06$0.56
1,000 posts, 400 with one image$5.00$0.60$5.60
1,000 posts with media resolving switched off$5.00$0.00$5.00

The run whose output is shown above requested 6 posts, delivered 4 and resolved 3 media files, for $0.02 — with the 2 unavailable posts costing nothing.

Turning Resolve media files off removes the media charge entirely, which is the cheapest way to run this at volume if you only need text and metrics.

FAQ

Do I need an X account, an API key or a developer application? No. The Actor reads X's own public post endpoint, which serves the same data an embedded post on any website shows. Nothing is logged in to.

Can it fetch a user's timeline, search results, or the replies to a post? No, and it does not pretend to. It resolves posts you name. It returns the number of replies a post has, but not the replies themselves. If you need a timeline or a search, this is the wrong Actor and you should not buy it for that.

Does it work on protected or deleted posts? It tells you they are protected or deleted. It cannot read them, and it does not charge you for finding out.

How long can the returned text be? Longer than the 280-character display limit — posts of 326 characters have been returned whole in testing, and isLongForm marks the posts X itself flags as long-form. Very long articles have not been verified past that length, so the honest answer is that textLength on every row tells you exactly what you got.

Does it return retweet counts? Not on the post itself — X's public endpoint does not include one. It is included on the parent post of a reply, where X does supply it. A field that is not served is left out rather than filled with a zero.

Is this affiliated with X? No. See the legal note below.

Why is my run smaller than my input list? Three reasons, all of them visible in the run log and the run summary: entries that were not post references, duplicates removed, and the Maximum tweets per run cap. The summary in the key-value store gives exact counts for each.

What this Actor is not

It is a resolver, not a discovery tool. It does not search X, does not walk a timeline, does not follow a thread, and does not read anything behind a login. It turns post references you already hold into structured rows. Every one of those boundaries is stated here rather than discovered after you have paid for a run.

Limits and troubleshooting

  • Rate. The default 250 ms pause between posts is polite and reliable. If you are running many thousands of posts, raise it rather than lowering it.
  • Media URLs expire. X's file URLs are stable for a useful window but are not permanent archives. Download what you need rather than storing the URL for later.
  • A run stopped early. If your event budget is reached mid-run, the Actor stops, says so in the log, and sets stoppedByEventBudget in the run summary — it never returns a short list that reads as a complete one.
  • Everything came back NOT_FOUND. Check the IDs. A post ID is the long number at the end of a status URL, not the numeric part of a username.

Integrations and API

The dataset is available in JSON, CSV, Excel, XML and RSS from the run's storage, and this Actor can be called from anywhere the Apify API reaches — including the Apify JavaScript and Python clients, scheduled runs, webhooks, and Make or Zapier via Apify's own connectors.

curl -X POST "https://api.apify.com/v2/acts/praise-most-high~x-twitter-bulk-tweet-extractor/runs?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tweetUrls":["https://x.com/jack/status/20"],"includeMedia":true}'

Because it runs on pay-per-event pricing with limited permissions, it can also be called by an AI agent through Apify's agentic payment flow.

This Actor is unofficial and is not affiliated with, endorsed by, or sponsored by X Corp., Twitter, or any of their subsidiaries. "X" and "Twitter" are trademarks of their respective owners and are used here only to describe what this Actor reads.

It accesses only publicly available post data — the same data any website shows in an embedded post — and it accesses no private, protected or logged-in content. You are responsible for how you use the data you collect, including compliance with the X Terms of Service and with data-protection law in your jurisdiction where the posts you collect contain personal data.

Other actors

This Actor is one of a small set on this account that read first-party public endpoints without an API key, alongside monitors for YouTube channel uploads and Telegram channel messages. Each reads one source and does one job.

Your feedback

Found a post this Actor handles badly, or a field you need that X serves and this does not return? Open an issue from the Actor's Issues tab on its Store page. Issues are read and answered, and a bug report with the post ID that triggered it is the fastest possible fix.