# Twitter Scraper | Extract Tweets & Profiles | No API Key (`cryptosignals/twitter-scraper`) Actor

Scrape Twitter/X profiles and tweets without an API key. Batch many accounts in one run, or fetch specific tweets by ID. Tweet text, author, likes, retweets, replies, views, media URLs and timestamps. No keyword search. PPE — pay only for results delivered.

- **URL**: https://apify.com/cryptosignals/twitter-scraper.md
- **Developed by:** [Web Data Labs](https://apify.com/cryptosignals) (community)
- **Categories:** Social media, AI, Lead generation
- **Stats:** 268 total users, 21 monthly users, 72.3% runs succeeded, 3 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 result scrapeds

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?

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

## Twitter/X Scraper — Extract Tweets & Profiles Without API Keys

Scrape public Twitter/X **profiles and tweets** — one account, a list of accounts, or specific tweets by ID — with no API key, no developer account and no login. Structured JSON out, pay only for the rows you receive.

> **Keyword search is not supported.** Anonymous keyword/hashtag search on X is auth-walled and this Actor will not pretend otherwise — see [Limitations](#limitations). If you need keyword search, you need the official X API or a logged-in scraper.

***

### Why Use This Instead of the Official Twitter API?

| | Official X API | This Actor |
|---|---|---|
| **Setup** | Developer account, application, approval | None — just run it |
| **Cost floor** | Paid developer tiers only | Pay per result |
| **Auth** | API keys, OAuth, token rotation | None |
| **Output** | Raw API JSON | Flat, analysis-ready rows |

X's official API is locked behind a paid developer account: the legacy Basic tier (~$200/mo) is closed to new signups, new developers are metered pay-per-use (~$0.005/read, ~2M-read cap), and anything above that jumps to Enterprise (~$42k+/mo). This Actor reads the same **public** data.

***

### What You Can Scrape

#### 1. One account

```json
{ "username": "nasa", "maxResults": 50 }
```

Returns the profile row plus up to 50 tweets.

#### 2. Many accounts in ONE run

```json
{ "usernames": ["nasa", "github", "apify"], "maxResults": 25 }
```

Each account contributes a profile row and its tweets. One run, one dataset — no orchestration on your side. `startUrls` accepts full profile URLs instead if that is what your pipeline already produces:

```json
{ "startUrls": [{ "url": "https://x.com/nasa" }, { "url": "https://x.com/github" }] }
```

#### 3. Specific tweets by ID or URL

```json
{ "tweetIds": ["1519480761749016577", "https://x.com/nasa/status/2084712464466190650"] }
```

Useful for re-hydrating a list of tweet IDs, refreshing engagement metrics on posts you already track, or pulling the exact posts a webhook told you about. Deleted, protected or non-existent tweets are **skipped and never charged**.

#### 4. Quoted tweets

```json
{ "username": "elonmusk", "maxResults": 50, "includeQuotedTweets": true }
```

When a scraped tweet quotes another tweet, the quoted tweet is returned as its own row (`type: "quoted_tweet"`, with `quoted_by_tweet_id` pointing back at the parent) — full text, author, engagement. It comes from the same API response, so it costs no extra time.

***

### Output

Every row carries a `type` field: `profile`, `tweet` or `quoted_tweet`.

#### Tweet row

```json
{
  "type": "tweet",
  "tweet_id": "2084712464466190650",
  "user_id": "11348282",
  "user_name": "NASA",
  "user_handle": "NASA",
  "user_followers": 92241569,
  "user_verified": true,
  "tweet_text": "LIVE: After 241 days in orbit, @astro_ChrisW returned home on July 26...",
  "likes": 1247,
  "retweets": 180,
  "replies": 112,
  "quotes": 13,
  "views": 776732,
  "bookmarks": 56,
  "tweet_url": "https://x.com/NASA/status/2084712464466190650",
  "created_at": "Tue Aug 04 18:45:58 +0000 2026",
  "is_retweet": false,
  "is_reply": false,
  "is_quote": false,
  "in_reply_to_tweet_id": "",
  "in_reply_to_handle": "",
  "conversation_id": "2084712464466190650",
  "language": "en",
  "hashtags": ["Artemis"],
  "mentions": ["astro_ChrisW", "Space_Station"],
  "links": ["https://www.nasa.gov/live"],
  "media_urls": ["https://pbs.twimg.com/media/....jpg"],
  "quoted_tweet_id": "",
  "quoted_tweet_text": "",
  "source": "twitter_api"
}
```

`hashtags`, `mentions`, `links`, `user_followers`, `user_verified`, `in_reply_to_*` and `conversation_id` are new in 1.0.19 — no input change needed, they simply appear.

#### Profile row

```json
{
  "type": "profile",
  "user_id": "11348282",
  "username": "nasa",
  "screen_name": "NASA",
  "name": "NASA",
  "bio": "Making the seemingly impossible, possible. ✨",
  "tweets_count": 74293,
  "following": 119,
  "followers": 92241569,
  "likes_count": 16904,
  "verified": true,
  "profile_image": "https://pbs.twimg.com/profile_images/.../..._400x400.jpg",
  "banner_image": "https://pbs.twimg.com/profile_banners/11348282/1775567134",
  "location": "Pale Blue Dot",
  "website": "http://www.nasa.gov/",
  "joined": "Wed Dec 19 20:20:32 +0000 2007",
  "listed_count": 97012,
  "media_count": 28060,
  "profile_url": "https://x.com/NASA",
  "source": "twitter_api"
}
```

`username` echoes what you passed in; `screen_name` is X's canonical casing.

***

### Input Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `username` | string | `elonmusk` | A single account (with or without `@`) |
| `usernames` | array | `[]` | Several accounts in one run |
| `startUrls` | array | `[]` | Profile URLs, e.g. `https://x.com/nasa` |
| `tweetIds` | array | `[]` | Tweet IDs or status URLs |
| `action` | string | `both` | `both`, `tweets` (no profile row), or `profile` (legacy alias for `both`) |
| `maxResults` | integer | `10` | Max tweets **per account**. `0` = profiles only |
| `includeQuotedTweets` | boolean | `false` | Emit quoted tweets as their own rows |

The Actor also accepts these non-search compatibility aliases and normalizes them to the canonical fields:

- `maxTweets`, `limit`, `count`, `max_tweets`, and `maxItems` are integer `0`–`10000` aliases for `maxResults`.
- `handle`, `user`, and `screen_name` are string aliases for `username`.
- `handles`, `users`, and `screen_names` are arrays of strings aliases for `usernames`.

When both a canonical field and one of its aliases are supplied, the canonical field wins. These compatibility aliases have no default or prefill. Keyword-search aliases — `searchQuery`, `query`, `searchTerms`, `searchQueries`, and `keywords` — plus `action: "search"` remain rejected; keyword search is unsupported.

***

### Use Cases

- **Brand monitoring** — track what a set of accounts posts, with engagement metrics, on a schedule.
- **Competitor research** — batch several competitor handles into one daily run and diff the output.
- **Influencer vetting** — `maxResults: 0` over a list of handles gives you follower counts, bios and links for a whole shortlist in one cheap run.
- **Engagement refresh** — feed yesterday's `tweet_id`s back in via `tweetIds` to see how likes/views moved.
- **Content curation** — `includeQuotedTweets` captures the conversation around a post, not just the post.
- **Academic research** — flat CSV/JSON export of public timelines without an API application.

***

### Code Examples

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("cryptosignals/twitter-scraper").call(run_input={
    "usernames": ["nasa", "github", "apify"],
    "maxResults": 25,
    "includeQuotedTweets": True,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["type"] == "profile":
        print(f'@{item["screen_name"]}: {item["followers"]:,} followers')
    else:
        print(f'  {item["likes"]:>6} likes  {item["tweet_text"][:70]}')
```

#### Node.js

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('cryptosignals/twitter-scraper').call({
    startUrls: [{ url: 'https://x.com/nasa' }, { url: 'https://x.com/github' }],
    maxResults: 25,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.table(items.filter(i => i.type === 'tweet').map(({ user_handle, likes, tweet_text }) => ({ user_handle, likes, tweet_text })));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/cryptosignals~twitter-scraper/runs?token=%3CTOKEN%3E" \
  -H 'Content-Type: application/json' \
  -d '{"usernames":["nasa","github"],"maxResults":20}'
```

***

### Pricing

Pay-per-event: **$0.005 per result**. A result is one row in the dataset — one profile, one tweet, or one quoted tweet.

**You are charged before a row is delivered, never after.** If you set `maxTotalChargeUsd` on a run, the Actor stops as soon as your cap is reached and tells you how many rows it delivered. You never receive rows you were not charged for, and you are never charged for rows you did not receive. Failed lookups (deleted tweets, suspended accounts, anti-bot pages) cost nothing.

| Scenario | Results | Cost |
|---|---|---|
| One account, 10 tweets | 11 | $0.055 |
| 10 accounts, profiles only (`maxResults: 0`) | 10 | $0.05 |
| 5 competitors × 100 tweets, daily | 505/day | ~$2.53/day |
| Re-hydrating 200 tweet IDs | 200 | $1.00 |

***

### Scheduling & Automation

Use **Schedule** in the Apify Console for recurring runs, and webhooks to push results downstream (Zapier, Make, your own endpoint, or another Actor). Results export as JSON, CSV, XLSX, XML or RSS.

***

### How It Works

The Actor reads X's public, unauthenticated endpoints using a guest token — the same data any logged-out visitor sees. It carries **two generations of X's internal query identifiers** and falls through to the second if the first is retired, and it parses both of X's user-object shapes, so a platform-side rotation degrades it rather than breaking it. A public mirror is used as a last-resort fallback for timelines.

A run that retrieves nothing exits with a **failed** status and a message explaining why. It never finishes green and empty, and it never writes a placeholder row.

***

### Limitations

> ⚠️ **Read this before buying.** These are platform restrictions of X's unauthenticated endpoints, not bugs, and no amount of retrying works around them.

- **No keyword or hashtag search.** X's `SearchTimeline` endpoint returns HTTP 404 without a logged-in session, and every public Nitter mirror now answers with an anti-bot challenge. Passing `action: "search"` or a keyword-search alias (`searchQuery`, `query`, `searchTerms`, `searchQueries`, or `keywords`) is rejected before a run starts rather than returning nothing. Verified 2026-08-05.
- **No followers/following lists, no reply timelines, no media tab, no full conversation threads.** All of these endpoints are auth-walled for anonymous clients (HTTP 404), verified against X's current query IDs on 2026-08-05.
- **Small accounts may return 0 tweets.** X's unauthenticated timeline endpoint does not serve timelines for very small accounts (roughly under ~10k followers). The profile row is still returned. Accounts at 10k+ followers were verified working.
- **Public data only** — no protected accounts, no DMs.
- **Max 10,000 tweets per account per run**, paginated automatically.
- **Rate limits** — X throttles public endpoints; very large jobs take longer.

***

### 📡 More social monitoring actors by this team

| Platform | Actor |
|---|---|
| Instagram | [Instagram Profile Scraper](https://apify.com/cryptosignals/instagram-profile-scraper) |
| Bluesky | [Bluesky Scraper](https://apify.com/cryptosignals/bluesky-scraper) |
| Reddit | [Reddit Scraper (Fast)](https://apify.com/cryptosignals/reddit-scraper-fast) |
| YouTube | [YouTube Scraper](https://apify.com/cryptosignals/youtube-scraper) |
| Pinterest | [Pinterest Scraper](https://apify.com/cryptosignals/pinterest-scraper) |

***

### FAQ

**Do I need an X API key or developer account?**
No. No key, no login, no OAuth.

**Can I scrape by keyword or hashtag?**
No — see Limitations. This used to be advertised and no longer works; the Actor now fails loudly instead of returning an empty dataset.

**How do I scrape multiple accounts?**
`usernames: ["a", "b", "c"]` or `startUrls`. One run, one dataset.

**Why am I getting 0 tweets for a username?**
Almost always account size — X does not serve timelines for very small accounts anonymously. You still get the profile row.

**Is this legal?**
It reads publicly available data with no authentication and no circumvention of access controls. You are responsible for complying with X's terms and with data-protection law in your jurisdiction.

**What happens when X changes their endpoints?**
The Actor carries two query-ID generations and two parser shapes, so a rotation usually degrades one path rather than breaking the run. When something does break, it fails loudly instead of billing you for empty rows.

**What is pay-per-event pricing?**
You pay per row delivered, charged before delivery. No monthly fee, no compute-unit maths.

# Actor input Schema

## `username` (type: `string`):

A single Twitter/X username to scrape (with or without @). For several accounts in one run, use 'Usernames' or 'Profile URLs' below.

## `handle` (type: `string`):

Non-search compatibility alias for 'username'. No default or prefill; 'username' takes precedence when both are supplied.

## `user` (type: `string`):

Non-search compatibility alias for 'username'. No default or prefill; 'username' takes precedence when both are supplied.

## `screen_name` (type: `string`):

Non-search compatibility alias for 'username'. No default or prefill; 'username' takes precedence when both are supplied.

## `usernames` (type: `array`):

Scrape several accounts in ONE run. Each account yields a profile row plus its tweets. Cheaper and faster than one run per account.

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

Non-search compatibility alias for 'usernames'. Each entry must be a string; no default or prefill. 'usernames' takes precedence when both are supplied.

## `users` (type: `array`):

Non-search compatibility alias for 'usernames'. Each entry must be a string; no default or prefill. 'usernames' takes precedence when both are supplied.

## `screen_names` (type: `array`):

Non-search compatibility alias for 'usernames'. Each entry must be a string; no default or prefill. 'usernames' takes precedence when both are supplied.

## `startUrls` (type: `array`):

Twitter/X profile URLs, e.g. https://x.com/nasa. Equivalent to 'Usernames' — use whichever your pipeline already produces.

## `tweetIds` (type: `array`):

Fetch specific tweets by ID (e.g. 1519480761749016577) or by status URL. Each available tweet is one result; deleted or protected tweets are skipped and never charged.

## `action` (type: `string`):

'both' (default) returns the profile row and the tweets. 'tweets' returns tweets only. 'profile' returns the profile plus tweets, as in earlier versions. Keyword search remains unsupported — anonymous search on X is auth-walled, so 'search' is not an option and action: 'search' is rejected before a run starts.

## `maxResults` (type: `integer`):

Maximum tweets to retrieve for EACH account. Set to 0 to fetch profiles only. Defaults to 10 when omitted. Non-search compatibility aliases such as maxTweets are accepted when maxResults is omitted; maxResults takes precedence when both are supplied.

## `maxTweets` (type: `integer`):

Non-search compatibility alias for 'maxResults'. Accepts an integer from 0 through 10000. No default or prefill; 'maxResults' takes precedence when both are supplied.

## `limit` (type: `integer`):

Non-search compatibility alias for 'maxResults'. Accepts an integer from 0 through 10000. No default or prefill; 'maxResults' takes precedence when both are supplied.

## `count` (type: `integer`):

Non-search compatibility alias for 'maxResults'. Accepts an integer from 0 through 10000. No default or prefill; 'maxResults' takes precedence when both are supplied.

## `max_tweets` (type: `integer`):

Non-search compatibility alias for 'maxResults'. Accepts an integer from 0 through 10000. No default or prefill; 'maxResults' takes precedence when both are supplied.

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

Non-search compatibility alias for 'maxResults'. Accepts an integer from 0 through 10000. No default or prefill; 'maxResults' takes precedence when both are supplied.

## `includeQuotedTweets` (type: `boolean`):

When a scraped tweet quotes another tweet, also return the quoted tweet as its own row (full text, author and engagement metrics). Each quoted tweet counts as one result.

## Actor input object example

```json
{
  "username": "elonmusk",
  "usernames": [],
  "startUrls": [],
  "tweetIds": [],
  "action": "both",
  "maxResults": 20,
  "includeQuotedTweets": false
}
```

# Actor output Schema

## `dataset` (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 = {
    "username": "elonmusk",
    "usernames": [],
    "startUrls": [],
    "tweetIds": [],
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("cryptosignals/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 = {
    "username": "elonmusk",
    "usernames": [],
    "startUrls": [],
    "tweetIds": [],
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("cryptosignals/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 '{
  "username": "elonmusk",
  "usernames": [],
  "startUrls": [],
  "tweetIds": [],
  "maxResults": 20
}' |
apify call cryptosignals/twitter-scraper --silent --output-dataset

```

## MCP server setup

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