# Twitter Scraper & API (X.com): Tweets, Posts, Search (`codeperfection/x-twitter-scraper`) Actor

Scrape X (Twitter) tweets and posts by keyword, hashtag, profile, list, URL or thread for OSINT and research. No X API key. JSON or CSV with text, author, engagement and media. $0.49 per 1,000 tweets, $0.46 with a Store discount; $0 for empty results. API and MCP. Unofficial, not affiliated with X.

- **URL**: https://apify.com/codeperfection/x-twitter-scraper.md
- **Developed by:** [CodePerfection](https://apify.com/codeperfection) (community)
- **Categories:** Social media, AI, Agents
- **Features:** Limited permissions
- **Stats:** 389 total users, 125 monthly users, 99.5% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.46 / 1,000 tweets

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

**A Twitter scraper for X (Twitter) with billing you can audit.** Scrape tweets and X posts by keyword, hashtag, profile, list, URL, or thread as structured JSON or CSV. **$0.49 per 1,000 tweets**, down to $0.46 with an Apify Store discount, charged once per unique tweet delivered, with no minimum per search and **no charge for empty results**. Every scraping run ends with a receipt where **accepted = charged = dataset items**. No mock data, and no duplicate tweets within a run.

No official Twitter/X API key required. Output is ready for apps, spreadsheets, OSINT and research workflows, AI agents, and LLM/RAG pipelines. X now calls tweets "posts"; this page uses both words for the same thing.

> **Unofficial.** This Actor is not affiliated with, endorsed by, or sponsored by X Corp or Twitter. "X" and "Twitter" are trademarks of their respective owners.

> **中文用户**：本页下方附有中文简介。

### At a glance

- **What it does:** searches X (Twitter) and returns real tweets as structured JSON or CSV. A Twitter API alternative with no developer account, app, or bearer token.
- **Input (any combination):** `searchTerms` (X advanced-search syntax), `twitterHandles`, `startUrls` (tweet, profile, search, list, or hashtag URLs), `conversationIds` (threads). Cap with `maxTweets` per term and `maxTotalTweets` per run.
- **Output:** one dataset item per unique tweet, with string `id`, `url`, `text`, `created_at`, engagement counts, `author`, media, and quoted and retweeted tweets expanded one level deep. A `RECEIPT` record reconciles what was charged.
- **Price:** $0.49 per 1,000 tweets delivered, down to $0.46 with an Apify Store discount. No charge for empty results and no minimum per search. Platform usage is included.
- **Free plan:** a one-time 10-tweet preview, then at most one 1-tweet sample every 20 hours. Paid Apify plans are uncapped.
- **Works with:** Apify Console, the API, Python and JavaScript clients, schedules, webhooks, and MCP clients such as Claude, ChatGPT, and Cursor. Actor ID: `codeperfection/x-twitter-scraper`.

***

### How much does it cost to scrape tweets?

**$0.49 per 1,000 tweets**, which is `$0.00049` per unique tweet delivered. Paid Apify plans get an Apify Store discount:

| Apify Store discount | Usual Apify plan | Price per 1,000 tweets |
|---|---|---|
| None | Free | $0.49 |
| Bronze | Starter | $0.48 |
| Silver | Scale | $0.47 |
| Gold or higher | Business or Enterprise | $0.46 |

Apify assigns the discount tier: its pricing page ties tiers to plans, and its Store terms base them on your total monthly Apify spend. The Pricing tab shows the price for your account. Platform usage (compute, proxy, storage) is included, so this Actor's only charge is the per-tweet price.

| Tweets delivered | No discount | Bronze | Silver | Gold or higher |
|---|---|---|---|---|
| 0 (empty run) | **$0** | **$0** | **$0** | **$0** |
| 100 | $0.049 | $0.048 | $0.047 | $0.046 |
| 1,000 | $0.49 | $0.48 | $0.47 | $0.46 |
| 10,000 | $4.90 | $4.80 | $4.70 | $4.60 |
| 100,000 | $49 | $48 | $47 | $46 |
| 1,000,000 | $490 | $480 | $470 | $460 |

For comparison, the official X API charges $0.005 per post read on its pay-per-usage plan, which is $5.00 per 1,000 posts ([X API pricing](https://docs.x.com/x-api/getting-started/pricing), checked 2026-10-08).

### Does it charge when a search returns no results?

No. You are charged once per unique real tweet delivered to your dataset, and for nothing else:

| Not billed | Why |
|---|---|
| Empty runs (0 results) | You pay for tweets, and there weren't any |
| Duplicate tweets | Deduplicated by ID within a run, across pages, sources, and search terms |
| Filtered-out tweets | Removed by your filters, never delivered |
| Unroutable inputs | Disclosed in `SKIPPED_SEEDS`, never charged |
| The free dry-run | Validate your output shape for $0 (see below) |
| Output shaping | Presets, projections, and jq transforms are included |

- **No minimum per search.** A search that returns one tweet costs one tweet, and there is no per-search fee.
- **A receipt on every scraping run.** The run writes a reconciliation record where `accepted = charged = dataset items`. If the three counts do not independently match, the run is marked failed and the receipt records the mismatch.
- **No mock or placeholder data.** Every row is a real tweet. Filtered-out, deduplicated, and unroutable inputs are disclosed in the run log, not padded into your billed dataset.

### What can you scrape from X (Twitter)?

One input covers search, profiles, hashtags, lists, single tweets, and threads, and everything in a run is merged and deduplicated into one billed result set:

- **Search (Twitter search scraper):** keywords, phrases, and X advanced-search operators (`from:`, `filter:`, `min_faves:`, `lang:`, hashtags).
- **Profiles (Twitter profile scraper):** a list of handles in `twitterHandles`, or profile URLs in `startUrls`. Each handle runs as a `from:` search, so your filters still apply.
- **Hashtags:** `#ai` as a search term, or a hashtag URL such as `https://x.com/hashtag/ai`.
- **Single tweets:** a tweet URL returns that exact tweet.
- **Lists and search URLs:** an X list URL or an X search URL in `startUrls`. The `sort` input sets the ranking; a search URL's `f=live` or `f=top` is ignored.
- **Replies and threads:** a conversation ID in `conversationIds` returns the replies in that thread.

### Ready-made examples

Each example is a saved configuration with its input filled in. Change the search, then run.

| Example | What it does |
|---|---|
| [Search X for tweets by keyword](https://apify.com/codeperfection/x-twitter-scraper/examples/search-tweets-by-keyword) | Keyword or advanced-search query, ranked by Top |
| [Scrape posts from an X profile](https://apify.com/codeperfection/x-twitter-scraper/examples/scrape-x-profile) | Latest tweets from one account |
| [Scrape tweets from multiple X accounts by date](https://apify.com/codeperfection/x-twitter-scraper/examples/scrape-tweets-from-multiple-accounts) | Several accounts in one run, limited to the last 30 days |
| [Scrape tweets by hashtag](https://apify.com/codeperfection/x-twitter-scraper/examples/scrape-tweets-by-hashtag) | Latest posts for a hashtag, tagged with the term that found them |
| [Monitor brand mentions in X posts](https://apify.com/codeperfection/x-twitter-scraper/examples/monitor-brand-mentions-on-x) | Mentions of an account in the last 24 hours, excluding its own posts |
| [Scrape replies to a tweet](https://apify.com/codeperfection/x-twitter-scraper/examples/scrape-replies-to-a-tweet) | The replies in one X thread |
| [Find high-engagement tweets](https://apify.com/codeperfection/x-twitter-scraper/examples/find-high-engagement-tweets) | Tweets above a like threshold in a date window |
| [Scrape X posts with images](https://apify.com/codeperfection/x-twitter-scraper/examples/scrape-x-posts-with-images) | Posts with images for a keyword, with media URLs |
| [X posts as compact JSON for AI agents](https://apify.com/codeperfection/x-twitter-scraper/examples/x-posts-json-for-ai-agents) | Smaller records for LLM context and RAG |
| [Export tweets to CSV](https://apify.com/codeperfection/x-twitter-scraper/examples/export-tweets-to-csv) | Flat spreadsheet columns |

### Filter tweets by date, likes, language, and media

Every filter you set in the input fields is applied as an X advanced-search operator **and re-checked on each returned tweet**. A tweet that doesn't match is dropped (counted as filtered, never delivered, never charged) rather than leaked through. If the data can't verify a filter on a returned tweet, that's disclosed in the run log; it's never silently ignored.

Date range, from/reply-to handle, mentions, minimum likes/retweets/replies, language, media type, verified / blue-verified, and quote-only are all enforced this way. Tweet IDs are returned as strings, so 64-bit IDs are never rounded; quoted and retweeted tweets are expanded one level deep.

You can also write X operators straight into a search term. These are passed to X search as written; only the input fields above are re-checked on each tweet.

| To get | Search term |
|---|---|
| Tweets from one account | `from:NASA` |
| Replies to an account | `to:NASA` |
| An exact phrase | `"climate change"` |
| Results without a word | `climate -crypto` |
| A date window | use the `startDate` / `endDate` fields (re-checked) |
| At least 100 likes | `climate min_faves:100` |
| One language | `climate lang:en` |
| Posts with images or videos | `climate filter:images` or `climate filter:videos` |
| No retweets or replies | `climate -filter:retweets -filter:replies` |

**What the counts mean.** Each search's `SUMMARY_*` record reconciles every tweet the query surfaced (fetched, kept, duplicates removed, filtered out, and unavailable) so the numbers always add up. Filtering is exact: a returned tweet that doesn't match is dropped, never leaked. What the Actor does *not* claim is completeness. X search itself indexes and ranks what it returns, so `kept` is an accurate count of what was fetched and passed your filters for this run, not a guarantee of every matching tweet ever posted. Raise `maxTweets` or narrow the window for deeper coverage; the `SUMMARY_*` records show exactly what you got.

### Input parameters

| Field | What it does |
|---|---|
| `searchTerms` | One or more searches, each with X advanced-search syntax |
| `twitterHandles` | Accounts whose tweets to collect (handle without the @) |
| `startUrls` | Tweet, profile, search, list, or hashtag URLs |
| `conversationIds` | Thread root tweet IDs whose replies to collect |
| `sort` | `Latest` (newest first, default) or `Top` (X's relevance ranking) |
| `maxTweets` | Cap per search term (default 100, maximum 10,000) |
| `maxTotalTweets` | Optional cap across the whole run (maximum 10,000) |
| `startDate` / `endDate` | Date window (`endDate` is exclusive): `YYYY-MM-DD`, an ISO-8601 timestamp, or relative such as `7 days` |
| `fromUser` / `toUser` / `mentioning` | Tweets by, replying to, or mentioning an account |
| `minFaves` / `minRetweets` / `minReplies` | Engagement thresholds |
| `language` | ISO 639-1 code such as `en`, `es`, or `de` |
| `media` | `any` (has any media), `images`, or `videos`; leave empty for no media filter |
| `onlyVerified` / `onlyBlueVerified` / `onlyQuote` | Author and tweet-type filters |
| `includeSearchTerms` | Adds a `search_term` field naming the term that found each tweet |
| `outputPreset` | `full`, `compact`, or `csvFlat` |
| `transformMode` | `none`, `projection` (pick and rename fields), or `jq` |
| `projection` / `jqExpression` / `jqVariables` / `flattenAuthor` | Settings for the field picker, jq mode, and flat author columns |
| `validateTransformOnly` | Free dry-run of your output shape: no scrape, no rows, no charge |

### Output: tweet fields

Each item is one real tweet (the `full` preset shown trimmed):

```json
{
  "id": "2075663637319217267",
  "url": "https://x.com/NatGeo/status/2075663637319217267",
  "text": "...",
  "created_at": "2026-07-10T19:29:09+00:00",
  "lang": "en",
  "like_count": 12043, "retweet_count": 1876, "reply_count": 402, "quote_count": 55, "view_count": 980112,
  "hashtags": [], "mentions": [], "urls": [], "media": [],
  "author": { "username": "NatGeo", "followers": 27926325, "verified": false, "blue_verified": true },
  "quoted": null, "retweeted": null
}
```

Prefer columns? Use `outputPreset: "csvFlat"`, or a projection / jq to keep exactly the fields you want.

### Export tweets to CSV or JSON

Shape and download the data however you need. It never changes your bill; you always get exactly **one row per tweet**:

- **Ready-made formats:** `full` (complete nested JSON), `compact` (the fields most people use), or `csvFlat` (CSV columns for spreadsheets and Excel). Values are not escaped against spreadsheet formula injection, so if you open the CSV in Excel or Google Sheets, treat cells starting with `=`, `+`, `-`, or `@` as untrusted.
- **Pick the fields you want (no code):** keep only the fields you need, rename them, reorder them, flatten nested fields into columns, and add your own tag columns.
- **Custom formulas (advanced):** a `jq` expression for calculated fields like total engagement, ratios, and conditionals.
- **Free preview:** set `validateTransformOnly: true` to see exactly what one row will look like against a sample tweet, with **no scrape, no rows, and no charge.**

### Twitter API alternative: no API key, from Python or HTTP

Scrape Twitter without an API key. There is no official X API access, developer account, app registration, or bearer token to set up, and no rate-limit tier to buy. Provide your search terms, handles, or URLs and run.

Search tweets from Python with the official `apify-client` package:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("codeperfection/x-twitter-scraper").call(
    run_input={"searchTerms": ["#ai lang:en min_faves:50"], "maxTweets": 100, "sort": "Latest"}
)
for tweet in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(tweet["created_at"], tweet["url"], tweet["text"][:80])
```

Or run it and get the tweets back in one HTTP call:

```bash
curl -s -X POST "https://api.apify.com/v2/acts/codeperfection~x-twitter-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer YOUR_APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"twitterHandles": ["NatGeo"], "maxTweets": 20}'
```

### Twitter MCP server for Claude, ChatGPT, and Cursor

Call this scraper straight from Claude, ChatGPT, Cursor, or any MCP client, with no code. Your agent finds it as a tool, runs it, and reads the tweets back for you.

**1. Connect.** The MCP server URL for this Actor is:

`https://mcp.apify.com/?tools=codeperfection/x-twitter-scraper`

- **Claude (desktop app or claude.ai):** open **Settings → Connectors**, select **Add**, then **Add custom connector**. Paste the URL above as the remote MCP server URL, select **Continue**, keep the sign-in settings Claude detects, and select **Add** ([Apify's Claude guide](https://docs.apify.com/integrations/claude-desktop)).
- **ChatGPT:** turn on Developer mode, then go to **Settings → Apps & Connectors → Create** and add a custom connector with the URL above ([Apify's ChatGPT guide](https://docs.apify.com/integrations/chatgpt)).
- **Cursor, VS Code, and other clients with a JSON config:**

```json
{
  "mcpServers": {
    "x-twitter-scraper": {
      "url": "https://mcp.apify.com/?tools=codeperfection/x-twitter-scraper",
      "headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }
    }
  }
}
```

**2. Ask.** Talk to your agent in plain language:

> "Get the 20 latest tweets from @NatGeo."
>
> "Search X for #AI posts in English with at least 100 likes."
>
> "Pull the replies in this thread: https://x.com/NatGeo/status/…"

Your agent runs the scraper and fetches the results for you. Billing is identical to the UI and API: **$0.49 / 1,000 tweets**, down to $0.46 with an Apify Store discount, and empty results cost **$0**.

**For agents using the general Apify MCP server.** Find this Actor with `search-actors` (for example "twitter scraper" or "x tweets search"), read its input schema with `fetch-actor-details`, then run it with `call-actor` using the Actor ID `codeperfection/x-twitter-scraper`. Put spending limits such as `maxTotalChargeUsd` in `callOptions`, not in the input. `call-actor` returns the run's storage IDs; read the tweets with `get-dataset-items` and the billing receipt with `get-key-value-store-record` (key `RECEIPT`). These storage tools are not enabled by default, so add them to the server URL, for example `https://mcp.apify.com/?tools=actors,storage`. This page is also available as Markdown at `https://apify.com/codeperfection/x-twitter-scraper.md`.

**Agents without an Apify account** can still use it: this Actor is enabled for Apify agentic payments (x402 and Skyfire), so an agent can pay per run without signing up. See [Apify's agentic payments documentation](https://docs.apify.com/integrations/x402) for the payment flow.

### Twitter OSINT and investigations

For open-source intelligence (OSINT), journalism, and research, the Actor collects public X posts with the details an investigation needs to cite and re-check them:

- **An account's posts in a date window:** `twitterHandles: ["handle"]` (or `fromUser`) with `startDate` and `endDate`. Dates are UTC; `endDate` is exclusive.
- **Exact posts by URL:** put tweet URLs in `startUrls` to fetch those posts. A post that is deleted, suspended, or protected is counted in the search's `SUMMARY_*` record (`tombstones`) and never charged.
- **Whole reply threads:** a conversation ID in `conversationIds` returns the replies in that thread.
- **Exact IDs:** every tweet ID, conversation ID, and author ID is a string, so 64-bit IDs are never rounded and always match the post on X.
- **An audit trail for every run:** each search writes a `SUMMARY_*` record (what was fetched, delivered, and left out by a cap), and the `RECEIPT` record reconciles tweets charged against tweets stored.
- **No X account or API key:** nothing ties a collection run to your own X account.

```json
{
  "twitterHandles": ["nasa"],
  "startDate": "2026-01-01",
  "endDate": "2026-02-01",
  "sort": "Latest",
  "maxTweets": 1000
}
```

The Actor collects what X search returns for public posts. It does not see private accounts, deleted posts, or direct messages, and it is not a completeness guarantee.

### Use cases

- **Brand and topic monitoring:** schedule a mentions or keyword search daily with `startDate: "1 day"` to collect up to `maxTweets` tweets from the last 24 hours on each run. A run that finds nothing costs $0. See [Monitor brand mentions](https://apify.com/codeperfection/x-twitter-scraper/examples/monitor-brand-mentions-on-x).
- **Research and OSINT:** date-bounded collection by account, phrase, or thread, with string tweet IDs and a reconciled receipt for every scraping run. See [Twitter OSINT and investigations](#twitter-osint-and-investigations).
- **AI agents, LLM context, and RAG:** compact JSON records, callable over MCP. See [X posts as compact JSON](https://apify.com/codeperfection/x-twitter-scraper/examples/x-posts-json-for-ai-agents).
- **Sentiment analysis and social listening datasets:** filter by language and engagement, then export to CSV.
- **Conversation analysis:** pull the replies to a post with [Scrape replies to a tweet](https://apify.com/codeperfection/x-twitter-scraper/examples/scrape-replies-to-a-tweet).

### Quick start

Search tweets by keyword or advanced-search operators:

```json
{ "searchTerms": ["\"climate change\" min_faves:100 lang:en", "#ai filter:images"], "maxTweets": 100, "sort": "Latest" }
```

Scrape a user's tweets (one or more handles):

```json
{ "twitterHandles": ["NatGeo", "BBCEarth"], "maxTweets": 50 }
```

A single tweet, a profile, and a thread, by URL / conversation ID:

```json
{
  "startUrls": ["https://x.com/NatGeo/status/2075663637319217267", "https://x.com/NatGeo"],
  "conversationIds": ["2073460397902827575"]
}
```

A hashtag or a search, by URL:

```json
{ "startUrls": ["https://x.com/hashtag/ai", "https://x.com/search?q=climate%20min_faves%3A100"], "maxTweets": 200 }
```

Input-field filters (applied by X and re-checked on every tweet):

```json
{ "searchTerms": ["climate"], "fromUser": "NatGeo", "startDate": "7 days", "minFaves": 100, "language": "en", "media": "images" }
```

### The receipt

At the end of every scraping run, the key-value store holds a `RECEIPT` record:

```json
{ "billingEvent": "apify-default-dataset-item", "runStatus": "completed", "acceptedUniqueTweets": 100, "chargedTweets": 100, "datasetItems": 100, "reconciled": true, "reconciliationStatus": "ok" }
```

`runStatus` is `completed` (every search finished, or stopped at one of your caps), `completed_with_errors` (at least one search failed, or ended short for a reason other than one of your caps, for example X stopped returning results), `failed`, `nothing_to_do` (no usable search was provided), or `cost_limit_too_low` (the maximum cost per run cannot cover one tweet with a clean finish); `costLimitTweetCeiling` is set when your maximum cost per run, rather than your tweet caps, limited the run. `datasetItems` is read back independently from the dataset, so the receipt reconciles what you were charged against what was stored, rather than restating one number twice. Per-term `SUMMARY_*` records break down what each search delivered vs. fetched; when a cap cut a search short, `delivery_reason` names it: `maxTweets`, `maxTotalTweets`, `cost_limit` (your maximum cost per run), `free_plan`, or `charge_budget` (Apify's charge limit stopped delivery mid-run). `jobsPartial` counts every search that stopped short, and `jobsCapped` is the part of it that stopped at a cap, so `jobsPartial` minus `jobsCapped` is the number of searches that ended short for another reason. When Apify's charge limit stops a run (`chargeBudgetReached: true`), Apify may also end the run as aborted.

### Free plan

Free-plan (non-paying Apify) accounts get a one-time preview of **10 tweets in total**, spread across as many runs as you like, so you can try the Actor end to end. After that, each free account gets at most **one 1-tweet sample every 20 hours**; a sample run that finds nothing does not use up the window. If the preview balance cannot be checked at the moment, a free run returns that 1-tweet sample instead of failing. Free-plan runs report `freeLifetimeRemaining`, the number of preview tweets you have left. Upgrade to any paid Apify plan for uncapped runs.

### FAQ

#### How do I scrape tweets without a Twitter/X API key?

Provide search terms, handles, or X URLs and run. There is no official X API, developer account, app, or bearer token to set up; the Actor collects the tweets for you.

#### How much does it cost to scrape 10,000 tweets?

$4.90 without a discount, $4.80 with the Bronze Apify Store discount, $4.70 with Silver, and $4.60 with Gold or higher. That is $0.49 per 1,000 unique tweets delivered (`$0.00049` each) before any discount, so 100,000 tweets cost $49 without a discount and $46 to $48 with one. Empty runs cost **$0**, and platform usage (compute, proxy, storage) is included.

#### Does it charge you when a search returns no results?

No. Billing is one charge per unique tweet delivered to your dataset. A search with no results costs **$0** and reports `no results`, and duplicates and filtered-out tweets are never charged. There is no minimum per search and no per-search fee.

#### Can I export tweets to CSV or JSON?

Yes. Set `outputPreset: "csvFlat"` for flat spreadsheet columns, keep `full` or `compact` for nested JSON, or use the field picker / jq to choose exactly the columns you want. Shaping the output never changes your bill.

#### How do I scrape a user's tweets?

Add the account to `twitterHandles` (or pass its profile URL in `startUrls`) and set `maxTweets` to how many you want. Each handle runs as a `from:` search, so your date, engagement, and language filters still apply. Coverage is bounded by `maxTweets` and how far back X search returns results for that account; each search's `SUMMARY_*` record shows how many were fetched.

#### How do I scrape tweets from a hashtag?

Put the term in `searchTerms` (for example `#ai`), or pass a hashtag URL like `https://x.com/hashtag/ai` in `startUrls`.

#### How do I scrape replies to a tweet?

Put the ID of the thread's first tweet (the number at the end of its URL) in `conversationIds`. The replies come back deduplicated, and your filters still apply.

#### Can I scrape X posts from a specific date range?

Yes. Set `startDate` and `endDate`. They are applied by X search and re-checked on every returned tweet. Date operators typed into a search term are passed to X as written and are not re-checked.

#### Can I monitor X on a schedule?

Yes. Save your input as a task and run it on an Apify schedule. With a daily schedule, a relative `startDate` of `1 day` limits each run to the last 24 hours, up to your `maxTweets` cap. Deduplication applies within a run, so a tweet on the boundary between two runs can appear (and be charged) in both. A run that finds no matching tweets costs $0.

#### How do I control cost?

`maxTweets` caps per search term; `maxTotalTweets` caps the whole run; and Apify's per-run max-cost limit is respected. If your maximum cost per run is what limits a run, the run delivers one tweet fewer than that limit covers, so it can finish cleanly instead of being stopped by Apify at the limit: set the limit at least one tweet above what you need ($0.00049, or less with an Apify Store discount; the run's status message names the exact amount).

#### What happens if I run it with no search?

The run finishes immediately as a $0 no-op with the message "Nothing to scrape", and nothing is charged. The same applies when none of the URLs you provide can be used; each skipped one is listed in `SKIPPED_SEEDS`.

#### What do "stopped at a cap" and "partial" mean in the run status?

"Stopped at a cap" means a search reached one of your limits (`maxTweets`, `maxTotalTweets`, your maximum cost per run, Apify's charge limit, or the free plan). You got what the limit allowed, and the receipt's `runStatus` stays `completed`; raise the cap to collect more. "Partial" means the search ended short for another reason, for example X stopped returning results before the search reached your cap, and `runStatus` is `completed_with_errors`. Either way you are charged only for the tweets delivered, and the search's `SUMMARY_*` record gives the details (`delivery_reason`, and how many fetched tweets a cap left out).

#### Is this a Twitter API alternative?

Yes. It returns the same kinds of tweet data without an X developer account, API tier, or bearer token, and you pay $0.49 per 1,000 tweets delivered, down to $0.46 with an Apify Store discount, with no developer account or API credits to manage. It is unofficial and collects what X search returns, so it is not a firehose or a completeness guarantee.

#### Is there a free tier?

Free-plan Apify accounts get a one-time 10-tweet preview, then at most one 1-tweet sample every 20 hours. Any paid Apify plan removes the cap.

#### Can I use it for OSINT investigations?

Yes. Collect an account's posts in a date range, fetch exact posts by URL, or pull a full reply thread, with string IDs and a per-run receipt. Deleted, suspended, and protected posts are counted in the run summary and never charged. See [Twitter OSINT and investigations](#twitter-osint-and-investigations).

#### Can AI agents use it?

Yes. Any MCP client, such as Claude, ChatGPT, Cursor, or VS Code, can call it through the Apify MCP server, and agents without an Apify account can pay per run through Apify agentic payments (x402 or Skyfire). See the MCP section above.

### 中文简介

**X（推特）推文采集工具，计费可对账**。按关键词、话题标签、账号、列表、链接或对话串采集 X 推文（X 现在把推文称为帖子），输出结构化的 JSON 或 CSV。无需 X 官方 API 密钥，也无需开发者账号。

- **价格**：每 1,000 条推文 0.49 美元，享受 Apify Store 折扣后最低 0.46 美元。只按实际交付到数据集的推文计费，同一次运行中同一条推文只计费一次；Apify 平台用量（计算、代理、存储）已包含在价格内。
- **无结果不收费**：搜索没有结果时费用为 0。单次搜索不设最低消费，运行内的重复推文和被筛选条件排除的推文都不计费。
- **每次采集运行都可对账**：运行结束时会生成 `RECEIPT` 对账记录，核对接受数、计费数与数据集条目数三者是否一致（`accepted = charged = dataset items`）。三者不一致时，该运行会被标记为失败，并在对账记录中注明差异。
- **筛选条件逐条校验**：在输入字段中设置的日期、语言、点赞数、媒体类型等筛选条件，会作为 X 高级搜索运算符发送，系统还会对每条返回的推文再次校验；不符合条件的推文直接丢弃，不会混入结果，也不计费。直接写在搜索词里的 X 运算符（例如 `lang:zh`）会原样传给 X 搜索，不做再次校验。
- **只返回真实数据**：不含模拟或占位数据。推文 ID 以字符串形式返回，不会因 64 位整数精度丢失而被舍入。
- **免费方案**：Apify 免费账户可获得共 10 条推文的一次性预览额度（可分多次运行使用），之后每 20 小时最多获取 1 条样本推文。升级到任意 Apify 付费方案后即不受此限制。
- **接入方式**：Apify Console、API、Python 和 JavaScript 客户端、定时任务、Webhook，以及 Claude、ChatGPT、Cursor 等 MCP 客户端。

用 Python 采集最新的中文推文：

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("codeperfection/x-twitter-scraper").call(
    run_input={"searchTerms": ["人工智能"], "language": "zh", "maxTweets": 100, "sort": "Latest"}
)
for tweet in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(tweet["created_at"], tweet["url"], tweet["text"][:80])
```

`language` 字段填写 ISO 639-1 语言代码，中文为 `zh`。本节仅为摘要。完整的输入参数、输出字段和计费说明请见上方英文部分，以英文内容为准。

> **非官方工具**。本 Actor 与 X Corp 或 Twitter 没有任何关联，也未获得其认可或赞助。X 和 Twitter 均为其各自所有者的商标。

### Changelog

- **2026-11-09**: Tweet URLs in `startUrls` are now fetched in batches of 50, so long URL lists no longer fail. A run now finishes before your run timeout instead of being stopped by it: it stops collecting with time left to deliver, charges only for what it delivered, and says so in the status message. `includeSearchTerms` now fills the `search_term` column even when your projection or jq also selects it, and custom jq output can use any value type in any field.
- **2026-10-15**: Added a short Simplified Chinese summary (中文简介) near the end of this page.
- **2026-10-08**: Apify Store discounts: $0.48 per 1,000 tweets with Bronze, $0.47 with Silver, and $0.46 with Gold or higher; $0.49 without a discount (unchanged). The charge is now listed as "tweet" instead of "result". A run where a search stopped at one of your caps (`maxTweets`, `maxTotalTweets`, your cost limit, or the free plan) now reports `runStatus` `completed` instead of `completed_with_errors`, and the receipt adds `jobsCapped`. Five new ready-made examples: brand mentions, thread replies, multiple accounts by date, posts with images, and compact JSON for AI agents.
- **2026-10-07**: Free plan changed from 5 tweets per run to a one-time 10-tweet preview plus at most one 1-tweet sample every 20 hours. The receipt adds the `nothing_to_do` and `cost_limit_too_low` statuses, and `delivery_reason` can now name `cost_limit` or `free_plan`.
- **2026-09-07**: If Apify restarts a run partway, it resumes from a checkpoint and does not charge again for tweets it already delivered.
- **2026-07-11**: Published on Apify Store.

### Support

Found a bug or need a field added? Open an issue on the Actor page. If this Actor saved you time, a review or a bookmark helps other people find it.

# Actor input Schema

## `searchTerms` (type: `array`):

One or more searches to run. Each term supports full X advanced-search syntax (for example "web scraping", "#ai", "apify lang:en"). Terms are paginated independently, and results are deduplicated by tweet ID across all terms, so you are not charged for the same tweet twice in a run. Provide at least one of: searchTerms, twitterHandles, startUrls, or conversationIds.

## `twitterHandles` (type: `array`):

Scrape tweets authored by these accounts (handle without the @). Each handle runs as a from: search, so your date, engagement, and language filters still apply. Deduplicated with everything else in the run.

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

Scrape directly from X URLs. Supported: a tweet URL (x.com/user/status/ID for that exact tweet), a profile URL (x.com/user for that user's tweets), a search URL (x.com/search?q=... for that query), a list URL (x.com/i/lists/ID), or a hashtag URL (x.com/hashtag/tag). An unrecognized URL is recorded in the run's SKIPPED_SEEDS record and not charged.

## `conversationIds` (type: `array`):

Expand one or more X threads. Give a conversation ID (the thread root tweet's ID) to get the replies in that conversation. Your filters still apply, and replies are deduplicated across the run like everything else.

## `sort` (type: `string`):

"Latest" is reverse-chronological (newest first). "Top" is X's relevance ranking.

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

Hard cap on real tweets returned per search term. You are charged only for the real tweets delivered, so a search that finds fewer than this (or none) costs proportionally less (or nothing).

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

Optional global cap on real tweets delivered across all search terms combined (maxTweets is the per-term cap). The run stops once this total is reached. Leave empty for no global cap.

## `startDate` (type: `string`):

Only tweets at or after this date (UTC). Accepts an absolute date (YYYY-MM-DD or ISO-8601 timestamp) or a relative value like "7 days" or "1 month". Applied server-side and re-checked on every returned tweet.

## `endDate` (type: `string`):

Only tweets strictly before this date (UTC). Accepts an absolute date (YYYY-MM-DD or ISO-8601 timestamp) or a relative value like "1 day".

## `fromUser` (type: `string`):

Only tweets authored by this account (handle without the @). Applied server-side (from:) and re-checked against the tweet author.

## `toUser` (type: `string`):

Only tweets that are a direct reply to this account (handle without the @). Matches the reply target, not arbitrary mentions.

## `minFaves` (type: `integer`):

Only tweets with at least this many likes.

## `minRetweets` (type: `integer`):

Only tweets with at least this many retweets.

## `minReplies` (type: `integer`):

Only tweets with at least this many replies.

## `language` (type: `string`):

Only tweets in this language, as an ISO 639-1 code (for example en, ru, es, de). A tweet with no detected language is dropped rather than returned.

## `media` (type: `string`):

Restrict to tweets that carry media of the chosen kind.

## `onlyVerified` (type: `boolean`):

Only tweets from verified accounts (legacy verified or X Premium/blue).

## `onlyBlueVerified` (type: `boolean`):

Only tweets from X Premium (blue) accounts. Stricter than 'Verified authors only'.

## `onlyQuote` (type: `boolean`):

Only quote tweets (a tweet that embeds another tweet). Applied with filter:quote and re-checked on every returned tweet.

## `mentioning` (type: `string`):

Only tweets that mention this account (handle without the @). Applied server-side and re-checked against each tweet's mentions.

## `includeSearchTerms` (type: `boolean`):

Add a `search_term` field to every tweet recording which of your search terms found it. Useful when running several terms in one run.

## `outputPreset` (type: `string`):

The base shape of every delivered tweet, before any advanced transform. "Full" is the complete nested JSON (all fields). "Compact" is a lean high-value JSON subset (author kept nested). "CSV flat" is flat scalar columns for CSV and spreadsheet tools (author flattened, arrays joined; structurally flat, but not formula-injection-sanitized, so see the docs if you import into a formula-evaluating app). One item out per tweet: the preset never changes how many tweets you get or what you are charged.

## `flattenAuthor` (type: `boolean`):

Legacy shortcut: replace the nested `author` object with flat `author_*` columns (author_followers, author_verified, ...). Applies to the Full and Compact presets; the CSV flat preset already flattens the author. Leave off to keep the nested JSON shape.

## `transformMode` (type: `string`):

Optional advanced step applied on top of the format. "None" delivers the format unchanged. "Pick the fields you want" lets you choose, order, rename, and flatten fields and add constant columns without code. "Custom formula (jq)" runs an advanced jq program for calculated and derived fields (sums, ratios, conditionals). None of them can change the number of items: every accepted tweet still produces exactly one output item. Settings for the inactive modes are checked for validity but not applied.

## `projection` (type: `object`):

Choose, order, rename, flatten, and add string constants without changing the number of output items. Ignored unless Reshape mode is 'Pick the fields you want'.

## `jqExpression` (type: `string`):

An advanced jq expression that reshapes each tweet into exactly one output object, for calculated and derived fields (engagement sums, ratios, conditionals, renames) beyond what the field picker can express. It runs on the preset-shaped tweet and must return exactly one JSON object per tweet: it cannot filter, drop, or fan out. An expression that returns nothing, a scalar, null, or multiple values is rejected before any paid run (a free dry-run is available). Module loading and environment or extra-input builtins are not permitted. Ignored unless Reshape mode is jq. Example: {id, text, engagement: (.like_count + .retweet_count + .reply_count)}

## `jqVariables` (type: `array`):

Optional string variables passed to the jq expression as $name (for example a campaign tag reused across the expression). Ignored unless Reshape mode is jq.

## `outputCardinality` (type: `string`):

Contract guarantee: exactly one dataset item is produced for each delivered unique tweet. Only "oneToOne" is supported.

## `validateTransformOnly` (type: `boolean`):

Validate the selected preset and transform against a fixed canonical sample tweet and write a preview to the run's Output, with no scrape, no dataset items, and no charge. Use it to check your transform before a paid run.

## `placeObjectId` (type: `string`):

Accepted only when empty, for compatibility with inputs copied from other X scrapers. No available data source returns the place ID needed to verify a place match, so a value is rejected.

## Actor input object example

```json
{
  "searchTerms": [
    "web scraping",
    "#ai"
  ],
  "twitterHandles": [
    "NatGeo",
    "BBCEarth"
  ],
  "startUrls": [
    "https://x.com/NatGeo/status/2075663637319217267",
    "https://x.com/NatGeo"
  ],
  "conversationIds": [
    "2073460397902827575"
  ],
  "sort": "Latest",
  "maxTweets": 10,
  "media": "",
  "onlyVerified": false,
  "onlyBlueVerified": false,
  "onlyQuote": false,
  "includeSearchTerms": false,
  "outputPreset": "full",
  "flattenAuthor": false,
  "transformMode": "none",
  "projection": {
    "selection": "onlyListed",
    "fields": [],
    "flatten": false,
    "constants": [],
    "arrays": "keep",
    "arrayJoinSeparator": ",",
    "missingFields": "omit"
  },
  "jqExpression": ".",
  "jqVariables": [],
  "outputCardinality": "oneToOne",
  "validateTransformOnly": false
}
```

# Actor output Schema

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

The scraped tweets stored in the default dataset, one item per unique real tweet delivered. See the dataset schema for per-field titles, descriptions, and examples (id, url, text, created_at, engagement counts, author, media, quoted, retweeted, and more).

## `receipt` (type: `string`):

A per-run reconciliation record in the default key-value store proving you were billed only for tweets actually delivered: acceptedUniqueTweets equals chargedTweets equals datasetItems, and reconciled is true only when the three counts independently match. Also reports userPlan, any free-plan cap, and for free-plan runs the remaining free-preview balance (freeLifetimeRemaining).

# 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 = {
    "searchTerms": [
        "apify"
    ],
    "maxTweets": 10,
    "jqExpression": "."
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeperfection/x-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 = {
    "searchTerms": ["apify"],
    "maxTweets": 10,
    "jqExpression": ".",
}

# Run the Actor and wait for it to finish
run = client.actor("codeperfection/x-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 '{
  "searchTerms": [
    "apify"
  ],
  "maxTweets": 10,
  "jqExpression": "."
}' |
apify call codeperfection/x-twitter-scraper --silent --output-dataset

```

## MCP server setup

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