# X (Twitter) Tweets Scraper — Any Account's Timeline (`thenetaji/x-tweets-scraper`) Actor

Collect everything a public X account has posted, starting from nothing but its handle. Every post arrives as one clean row: what was said, when it was said, and how far it travelled. Several accounts in one run, and no X login, cookie or account anywhere in it.

- **URL**: https://apify.com/thenetaji/x-tweets-scraper.md
- **Developed by:** [The Netaji](https://apify.com/thenetaji) (community)
- **Categories:** Social media, Automation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.34 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## X (Twitter) Tweets Scraper

Collect everything a public X account has posted, starting from nothing but its handle. Every post
comes back as one clean row: what was said, when it was said, and how far it travelled. The author's
follower count rides along on each row, so reach arrives with the post instead of costing a second
lookup per account.

No X login, cookie or session is involved at any point in a run. Nothing has to be connected, and no
account of anyone's is put at risk to collect a timeline.

Several accounts can be collected in one run. Bare handles, @handles and pasted profile links are all
accepted, and a single list may mix them.

### Accepted input

`handles` is required and takes one or more accounts. `maxItems` caps how many rows are saved across
the whole run rather than per account, and defaults to 100; setting it to `0` removes the cap.
`resumeCursor` continues a previous run instead of starting at the top of a timeline.
`enrichParentPost` attaches the post that each reply is answering, and is off by default.

```json
{
  "handles": ["nasa", "https://x.com/SpaceX"],
  "maxItems": 200
}
```

### Response fields

One row per post, newest first.

| Field | Contents |
|---|---|
| `tweet_id`, `tweet_url` | The post's identifier, and a link to it |
| `text` | The post body; `is_retweet` and `is_long_post` qualify it |
| `is_long_post` | `true` when `text` is a long post's complete body rather than its first 280 characters |
| `posted_at`, `lang` | Publish time in X's own date format, and X's language guess |
| `author_id`, `author_handle`, `author_name` | The account that posted it |
| `author_followers_count` | That account's follower count, on every row |
| `author_verified`, `author_is_blue_verified` | Legacy check and current mark |
| `author_profile_image_url` | The author's avatar |
| `like_count`, `retweet_count`, `reply_count`, `quote_count` | Engagement counts |
| `bookmark_count`, `view_count` | Bookmarks and impressions, both as numbers |
| `is_retweet`, `retweeted_tweet` | Whether the row is a retweet, and the original in full when it is |
| `is_quote`, `quoted_tweet` | Whether the post quotes another, and that post in full |
| `conversation_id`, `reply_to_tweet_id`, `reply_to_handle` | Position within a thread |
| `entities` | X's own `hashtags`, `urls`, `user_mentions`, `symbols` and `media` structure |
| `possibly_sensitive` | X's sensitive-content flag |
| `requested_handle` | The handle the run asked for, before X re-cased it |
| `reply_to_tweet` | The post this row replies to; present only when `enrichParentPost` is on |

```json
{
  "tweet_id": "2087966752512561302",
  "tweet_url": "https://x.com/NASA/status/2087966752512561302",
  "text": "Yesterday's total solar eclipse in parts of Europe – and the partial eclipse in parts of North America – gave us spectacular views. @NASAHQPhoto was on location to capture the sights. See more: https://t.co/IQ6yWeVbXl",
  "posted_at": "Thu Aug 13 18:17:21 +0000 2026",
  "lang": "en",
  "author_handle": "NASA",
  "author_followers_count": 92309471,
  "like_count": 16970,
  "reply_count": 258,
  "view_count": 1792803,
  "bookmark_count": 1180,
  "is_retweet": false,
  "requested_handle": "nasa"
}
```

### Questions that come up

**Why did a run save fewer posts than `maxItems`?**

X treats a requested page size as a hint rather than an instruction, and it decides when to stop
handing out pages. Over eight consecutive pages of one account, asking for twenty posts each time,
X delivered 12, 17, 18, 14, 16, 16, 16 and 13: 122 posts, zero duplicates, and not one page returning
the number asked for. A run therefore ends when X stops offering the next position, which can happen
before `maxItems` is reached. It never ends merely because a page came back thin; a walk that treated
a short page as the end would stop on page one and report twelve posts for an account holding tens of
thousands.

**How far back into a timeline does a run reach?**

Further than any number I am willing to print. The measured walk above reached 122 posts and was
still being offered another page, so it was never run to exhaustion; the depth is a floor rather than
a ceiling. The `tweet_count` field on an account's profile record is how many posts X states the
account has made, not how many a timeline walk reaches, and it should not be used as a budget for a
run.

**Can a long export be picked up where it stopped?**

Yes. A resume token is printed to the run log on every page that offers one, and supplying the last
one as `resumeCursor` continues from that position rather than re-walking the top of the timeline. A
token is a position within one account's stream and carries no meaning in another's, so it applies to
the first account in the list and is ignored for the rest; resuming is done one account at a time.

**Why does `text` start with `RT @` on some rows?**

Because the row is a retweet, and that truncated form is X's own. The original post is published
whole on the same row under `retweeted_tweet`, with its own author, engagement counts and untruncated
text, so nothing is lost. Quote posts behave the same way: `text` is the commentary and
`quoted_tweet` is the post being commented on. Both halves are published rather than one being
substituted for the other, because they answer different questions: the outer record is what the
account did, and the nested one is what it amplified.

**Are long posts cut off at 280 characters?**

No. When `is_long_post` is `true`, `text` is the complete body. X's ordinary post field would return
a 4,000-character post as its first 280 characters, with no ellipsis and no marker, which is a
truncation that reads exactly like a complete post; that is the failure the flag exists to prevent.

**Where are the images and videos on a post?**

Inside `entities`, in X's own structure, alongside `hashtags`, `urls`, `user_mentions` and `symbols`,
each carrying the character offsets into `text` where it appears. Anything already written against a
Twitter payload reads those keys as they stand. There is deliberately no flattened column of media
links beside them: the shape varies by attachment type, and publishing one column would mean guessing
at key names I have not measured.

**Can search results, the replies under a post, follower lists or a media tab be collected?**

No, and not by any setting. Each of those needs a logged-in X account, and this Actor holds none.
That is the trade for an Actor that asks for no login, no cookie and no session, and it is a measured
boundary rather than something still to be built. `reply_count` still reports how many replies X
counts on a post, `entities` still carries a post's media, and a thread is still walkable upward:
`reply_to_tweet_id` is an ordinary post identifier.

**What does `enrichParentPost` actually cost?**

One extra request per reply, billed once per reply whose parent is returned. Rows that are not
replies are untouched and cost nothing, so the option is close to free on an account that mostly
posts and roughly doubles the request count on an account that mostly replies. A parent that has been
deleted, or that sits on a protected account, leaves the reply row unchanged and is not billed. The
same hop can be taken by hand afterwards, since `reply_to_tweet_id` is a plain post identifier; the
option exists to do it inside one run rather than two.

**What happens to a handle that is wrong, retired, or protected?**

A handle outside X's own rule of 1 to 15 letters, digits or underscores is reported in the run log
and skipped before a request is spent on it. A handle that does not resolve to any account is
likewise reported and skipped, because X answers an unknown account as a normal empty response rather
than as an error and there is nothing to retry. A protected account returns an empty timeline, since
this Actor holds no X account and follows nobody. In every case the remaining accounts in the list
are still collected. Duplicates are collapsed before any request is made, so `nasa`, `@NASA` and
`https://x.com/nasa` in one list are one account and one set of charges.

**Why is `view_count` empty on some posts?**

Because X published no view count for them, which is normal on older posts. Null means that no count
was stated and is never collapsed to `0`, so an average over the column is not quietly dragged
towards zero by posts that simply predate the metric. Views and bookmarks are both numbers rather
than text, so sorting on them orders `54752` above `9` instead of the reverse.

**Are missing fields dropped from a row?**

No. Every row has the same shape, and a field absent upstream is `null` rather than omitted. The
three flags a reader has to branch on are the exception in the other direction: `is_retweet`,
`is_quote` and `is_long_post` are always `true` or `false`, never null, so a check on one never has
to handle a third case.

**Which column joins back to the input list?**

`requested_handle`. X returns handles in its own casing, so a run asking for `nasa` produces `NASA` in
`author_handle`; `requested_handle` echoes the value supplied, reduced from an @handle or a pasted
profile link, so a join never depends on two sides agreeing about normalisation.

If a field returns null where a value is clearly present on x.com, the Actor's Issues tab is the
fastest way to reach me; a post identifier in the report is usually enough to reproduce it.

### Related Actors

[X (Twitter) Profile Scraper](https://apify.com/thenetaji/x-profile-scraper) returns the accounts
themselves rather than their posts, including the bio, join date, and the full set of counts X
publishes. [X (Twitter) Post Scraper](https://apify.com/thenetaji/x-post-scraper) takes post links
directly and returns the same row shape as this Actor, which makes it the better choice once specific
posts are known; for a whole timeline this Actor is the cheaper one, because a single request returns
a page of posts rather than one.

# Actor input Schema

## `handles` (type: `array`):

One or more X accounts. Bare handles (`nasa`), @handles (`@nasa`) and profile links (`https://x.com/nasa`) are all accepted, and a single list may mix them. A handle outside X's own rule of 1-15 letters, digits or underscores is reported in the run log and skipped rather than ending the run.

## `maxItems` (type: `integer`):

Rows to save across all accounts in this run. Set 0 for no limit.

## `enrichParentPost` (type: `boolean`):

Fetches the post that each reply is answering and attaches it in full, with its own text, author and engagement counts. Rows that are not replies are unaffected and cost nothing. One extra request per reply, billed per reply whose parent is returned; a parent that has been deleted, or that sits on a protected account, is not billed.

## Actor input object example

```json
{
  "handles": [
    "nasa",
    "https://x.com/SpaceX"
  ],
  "maxItems": 50,
  "enrichParentPost": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

All records scraped by this run

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "handles": [
        "nasa"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/x-tweets-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "handles": ["nasa"],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/x-tweets-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "handles": [
    "nasa"
  ],
  "maxItems": 50
}' |
apify call thenetaji/x-tweets-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thenetaji/x-tweets-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/zYz2itPea1T5jLG9R/builds/zVFfbVIv9Vb5xSeRB/openapi.json
