# Twitter / X Scraper - Tweets, Profiles & Engagement (`eiv/twitter-scraper`) Actor

Scrape tweets from any public Twitter / X account by handle or URL. Full text, likes, retweets, replies, quotes, bookmarks and view counts, plus hashtags, mentions, expanded links, images and playable video URLs. Retweets carry the original post's engagement. No API key, no browser.

- **URL**: https://apify.com/eiv/twitter-scraper.md
- **Developed by:** [Eimantas V](https://apify.com/eiv) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 tweet scrapes

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Twitter / X Scraper — Tweets, Profiles & Engagement

Pull the posts from any public X (Twitter) account, with the numbers that matter attached:
likes, retweets, replies, quotes, bookmarks and view counts. No login, no API key, no
browser, and no X developer account.

Give it a handle, an `@handle`, or a profile URL. You get one row per tweet and one row per
account.

### What you get for every tweet

| Field | Notes |
|---|---|
| `text` | Full post text, including long posts that X truncates elsewhere |
| `createdAt` | ISO 8601, UTC |
| `likeCount`, `retweetCount`, `replyCount` | |
| `quoteCount`, `bookmarkCount`, `viewCount` | Views as X reports them |
| `isRetweet`, `isReply`, `isQuote` | |
| `retweetedFrom` | The account a retweet came from |
| `authorHandle`, `authorName`, `authorFollowers`, `authorVerified` | |
| `hashtags`, `mentions`, `links` | Links are expanded, not `t.co` |
| `mediaUrls`, `mediaType` | `photo`, `video` or `animated_gif` |
| `videoUrls` | Playable MP4s, highest quality first — `mediaUrls` holds only the poster frame for a video |
| `lang`, `url`, `tweetId` | |

**Retweets report the original post's engagement.** X hands a retweet its own counters set
to zero; this Actor reads through to the post that was retweeted, so a retweet of something
with 805 likes says 805, not 0. That is the difference between usable data and a silently
wrong column.

### And for every account

`displayName`, `description`, `followers`, `following`, `tweetsTotal`, `listedCount`,
`location`, `website`, `joinedAt`, `verified`, `profileImageUrl`, `bannerUrl` — plus
`tweetsFetched`, `pagesFetched` and `stoppedOn`, so you can always tell why a run returned
what it did.

### Pricing

Pay per result. You are charged for tweets you actually receive.

| Event | Price |
|---|---|
| Tweet scraped | $0.40 per 1,000 |
| Profile scraped | $0.002 each |
| Actor start | $0.00005 |

Accounts that do not exist, are suspended, or could not be reached are **never charged** —
results are written to your dataset before any charge is raised, so you cannot be billed for
something you did not get.

### Example input

```json
{
  "profiles": ["nasa", "@Interior", "https://x.com/NASAWebb"],
  "maxTweetsPerProfile": 100,
  "maxTotalTweets": 1000,
  "includeRetweets": true
}
```

### Example output

```json
{
  "tweetId": "2100212144692179390",
  "url": "https://x.com/nasa/status/2100212144692179390",
  "text": "LIVE: Tune in as an uncrewed cargo spacecraft lifts off to the @Space_Station…",
  "createdAt": "2026-09-16T13:16:05.000Z",
  "authorHandle": "NASA",
  "authorFollowers": 92368095,
  "isRetweet": false,
  "likeCount": 1969,
  "retweetCount": 353,
  "viewCount": 1647803,
  "mentions": ["Space_Station"],
  "mediaType": null,
  "lang": "en"
}
```

### Limits worth knowing before you run it

These are X's, not this Actor's, and no scraper can get around them:

- **About 3,200 posts per account.** X's own timeline stops there. Asking for more returns
  what exists and says so in `stoppedOn`.
- **Replies are not in the Posts timeline.** X serves an account's replies on a separate tab
  this Actor does not read, so `includeReplies` only decides whether a reply that appears
  anyway is kept. It cannot fetch an account's reply history.
- **Protected accounts return no posts.** The profile row still comes back, with real
  follower counts and `stoppedOn: "protected"`, so you can tell "private" from "empty".
- **About 10 tweets per request.** X returns 20 timeline entries whatever page size is
  asked for, and only some of them are posts — the first page of a timeline yields around
  17, deeper pages fewer. Measured over a full run: 300 tweets took 29 requests. Speed is
  bounded by that, not by this Actor.

### Speed and blocking

X rate-limits by address. The default settings run comfortably; if a run starts getting
refused, raise **Delay between requests** or switch the proxy to the **RESIDENTIAL** group
in the input — no rebuild needed. Guest sessions are renewed automatically when X expires
them.

### Integrations

Results land in a standard Apify dataset, so you can export to JSON, CSV or Excel, or push
them onward with any Apify integration — Make, Zapier, Google Sheets, Slack, or the API.
Two ready-made views are included: **Tweets** and **Profiles**.

### Support

Something wrong, or a field you need that isn't here? Open an issue on this Actor's page —
that reaches the developer directly and is the fastest way to get it fixed.

### Is scraping X legal?

This Actor reads only public posts — the same content any logged-out visitor sees — and
touches no private data and no account of yours. Scraping publicly available information is
broadly lawful, but what you do with it is not automatically so: personal data carries
obligations under the GDPR and similar laws, and X's own terms apply to you as a user. If
you are collecting personal data at scale, take your own legal advice first.

# Actor input Schema

## `profiles` (type: `array`):

Handles or profile URLs. "nasa", "@nasa" and "https://x.com/nasa" are all accepted.

## `maxTweetsPerProfile` (type: `integer`):

X returns 20 timeline entries per request whatever page size is asked for, which works out at roughly 10 tweets per request over a run. 100 tweets is about 10 requests. Its own timeline stops at about 3,200 posts per account.

## `maxTotalTweets` (type: `integer`):

A ceiling across every profile in the run, so a long list cannot run away with your budget.

## `includeRetweets` (type: `boolean`):

Retweets carry the original post's text and engagement, with retweetedFrom naming the account it came from. Turn off for original posts only.

## `includeReplies` (type: `boolean`):

X's Posts timeline does not serve replies, so this only decides whether a reply that appears anyway is kept. It cannot make this Actor fetch an account's reply history.

## `requestDelayMs` (type: `integer`):

X rate-limits by address. Raise this if runs start being refused.

## `proxyConfig` (type: `object`):

Datacenter works today and is far cheaper. Switch to RESIDENTIAL if X starts refusing the run.

## Actor input object example

```json
{
  "profiles": [
    "nasa",
    "@Interior"
  ],
  "maxTweetsPerProfile": 100,
  "maxTotalTweets": 1000,
  "includeRetweets": true,
  "includeReplies": false,
  "requestDelayMs": 700,
  "proxyConfig": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `tweets` (type: `string`):

No description

## `profiles` (type: `string`):

No description

# 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 = {
    "profiles": [
        "nasa"
    ],
    "proxyConfig": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("eiv/twitter-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 = {
    "profiles": ["nasa"],
    "proxyConfig": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("eiv/twitter-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 '{
  "profiles": [
    "nasa"
  ],
  "proxyConfig": {
    "useApifyProxy": true
  }
}' |
apify call eiv/twitter-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,eiv/twitter-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/yOhKtLt7dgs7rmuiz/builds/AVCI8aEzycbFp9gM9/openapi.json
