# X (Twitter) Tweet Search Scraper — Keyword, Hashtag & Account (`scrapersdelight/x-tweet-search-scraper`) Actor

Search X (Twitter) for tweets by keyword, phrase, hashtag, from:account or date window — or pull an account's latest tweets. Rows carry text, author, created date, likes, replies, retweets, media, links and quoted tweet. No login, no cookies, no bought feed.

- **URL**: https://apify.com/scrapersdelight/x-tweet-search-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Social media, Lead generation, Agents
- **Stats:** 5 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 per tweet returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## X (Twitter) Tweet Search Scraper — Keyword, Hashtag & Account

Search [X (Twitter)](https://x.com) for tweets by **keyword, exact phrase, #hashtag, from:account
or date window**, or pull an account's **latest tweets** — and get one clean row per tweet:
**id, url, text, createdAt, lang, authorHandle, authorName, authorId, authorVerified, likes,
replies, hashtags, mentions, urls, media** (direct image and MP4 URLs), reply / quote / retweet
flags and targets, the Google rank and snippet that found it, plus **retweet and quote counts**
and the author's **followers, bio and website** on account-mode rows. **No login. No cookies. No account pool. No bought feed. No CAPTCHA solving.**

**Scope, measured 2026-08-22:** search mode reads **Google's index of x.com** through Apify's
Google SERP proxy and hydrates every hit from **X's own public embed API** — 24/24 result pages
usable, 138 distinct tweets across 8 queries, 15/15 hydrations on every proxy rung. Account mode
reads X's public embedded timeline — **16/16 accounts, 644 tweets, 17 seconds** through the
default datacenter proxy. Those are counts read off live runs, not quoted from a marketing page.

```json
{
  "searchTerms": ["bitcoin"],
  "maxTweets": 100,
  "maxPagesPerTerm": 10
}
```

Click **Try for free** and press **Start** — that block is the input the Actor ships with. A run
on exactly those defaults returned **71 tweets in 75 seconds** (10 Google pages, 71 links, 71
hydrated, 0 failures, 0 duplicates), with text 100%, likes 100%, replies 100%, and media on 62.9%
of rows in the fill measurement below.

***

### Read this before you buy rows — what "search" means here

X shut logged-out search in 2023. `x.com/search` is a JavaScript shell, the guest-token GraphQL
search operation answers 404, the official `/2/tweets/search` endpoint is 401 without a paid
bearer, and X's logged-out web client ships **zero** GraphQL operations (all measured on
2026-08-22 on direct, Apify datacenter and residential). That leaves the other tweet-search Actors
on the Store two choices: resell a licensed feed (the 256 MB "no proxy required" ones), or run a
pool of logged-in accounts. This Actor does neither.

1. **Search mode returns the tweets Google has indexed, not the live firehose.** Google indexes
   the popular and recent tweets from active accounts — on the shipped `bitcoin` run the rows
   spanned 2017 to today, with a median of ~860 likes (a few big accounts pull the mean above
   2,000). If you need every tweet posted in the last ten minutes, this is the wrong tool; if you
   need the tweets people actually saw, it is the cheap and honest one.
2. **`retweets` and `quotes` are `null` on search-mode rows.** X's embed API publishes likes and
   reply counts, not retweet or quote counts. Account-mode rows carry all four (68.3% of the
   mixed-run rows below had real retweet counts — exactly the account-mode share).
3. **Account mode is the newest 20 tweets for most accounts, up to 100 for high-volume ones**
   (elonmusk, nytimes, sama and reuters returned 99-100; nasa, github, stripe returned 20-21).
   That is what X's embedded-timeline page publishes; there is no pagination beyond it.
4. **`views` does not exist in this dataset.** Neither public surface publishes impression counts,
   so there is no `views` column rather than an always-null one.
5. **A bare `#hashtag` is quoted for you.** Google drops a bare `#ai`; the Actor sends `"#ai"`,
   which measured 9 tweet links on page 1 against 4 for the bare form.

***

### The wedge: an open, logged-out transport with zero upstream cost

Every number in this section was measured through Apify's proxies on 2026-08-22.

- **No feed licence, no account pool.** The two Store leaders run at 256 MB with "no proxy
  required" — that is a bought search feed, resold at $0.25-0.40 per 1,000 tweets with the
  margin of a reseller and the fragility of one (the biggest of them aborts or times out on 5.7%
  of its runs — Apify store stats, 2026-08-27). This Actor's two surfaces are the ones X serves to
  every embedded tweet and timeline on the web, so there is nothing to expire, nothing to ban and
  nothing to re-license.
- **A real date window.** `since` / `until` become Google `after:` / `before:` — verified live:
  `before:2025-01-01` returned tweets dated Feb 2024 and Sep 2021, `after:2026-08-15` returned
  only that week. `recency` adds Google's past-day/week/month/year window.
- **A real language filter.** `language: "es"` sends `lr=lang_es`; measured, the page returned
  Spanish-language accounts only (Udelalibertad, elEconomistaes, Forbes\_Mexico …).
- **X search operators that just work.** `from:OpenAI gpt`, `"#bitcoin" etf since:2026-08-01`,
  `lang:de`, `min_faves:1000` and Google's `-word` are all translated for you (`from:` becomes
  `site:x.com/handle`).
- **Account-mode rows are the richest public tweet record there is:** likes, replies, retweets,
  quotes, conversation id, reply target, quoted tweet, retweet origin, full media with MP4 URLs,
  **and** the author's followers, following, tweet count, bio, website and account creation date.
- **Author enrichment as a success-billed event.** Turn on `enrichAuthors` and search-mode rows
  get the same author block (followers, bio, website, location, account age) — charged once per
  unique author whose profile was found, never per tweet, never on a miss.

As of 2026-08-22, "twitter search" on the Apify Store is held by feed resellers and account-pool
scrapers; none documents a logged-out transport. Judge this page on the measurements below rather
than on that claim.

***

### What you get — one row per tweet, one schema for both modes

| Group | Fields | Example |
|---|---|---|
| **Identity** | `id`, `url`, `text`, `createdAt`, `lang` | `2085434712429052386` · `2026-08-06T18:35:55.000Z` · `en` |
| **Author** | `authorHandle`, `authorName`, `authorId`, `authorVerified`, `authorProfileImage` | `OpenAI` · `4398626122` · `true` |
| **Author profile** (account mode / enriched) | `authorFollowers`, `authorFollowing`, `authorTweetCount`, `authorBio`, `authorWebsite`, `authorLocation`, `authorCreatedAt`, `authorEnriched` | `5121285` · `https://openai.com` · `2015-12-06` |
| **Engagement** | `likes`, `replies`, `retweets`, `quotes` | `21311` · `1384` · `1919` · `1116` |
| **Structure** | `isRetweet`, `isReply`, `isQuote`, `inReplyToHandle`, `inReplyToTweetId`, `quotedTweetId`, `quotedTweetUrl`, `retweetedTweetId`, `retweetedFromHandle`, `conversationId` | |
| **Entities** | `hashtags[]`, `mentions[]`, `urls[]` (expanded, t.co stripped), `media[]` `{type, url, videoUrl, width, height}`, `mediaCount`, `possiblySensitive` | `video` · `…/vid/avc1/1672x942/….mp4` |
| **Provenance** | `source` (`search` | `account`), `searchTerm`, `googleRank`, `googleSnippet`, `scrapedAt` | `search` · `bitcoin` · `null` |

All dates are ISO-8601 UTC. Counts are integers; `null` means the surface did not publish the
number (see the limits), `0` is a real zero. The dataset ships two saved **table views**: *Tweets*
(the overview) and *Authors* (handle, followers, bio, website, location — populated on
account-mode rows, and on search-mode rows when `enrichAuthors` is on).

***

### Field fill — measured on real rows

**Search mode — the shipped default (`bitcoin`, 62 rows, 2026-08-22).** Sorted by fill; the
sparse fields are the ones to plan around.

| Field | Fill | Notes |
|---|---|---|
| `id` / `url` / `text` / `createdAt` / `lang` | 100% | |
| `authorHandle` / `authorName` / `authorId` / `authorVerified` / `authorProfileImage` | 100% | |
| `likes` / `replies` | 100% | from X's embed API |
| `googleRank` / `googleSnippet` | 56-100% | Google now hides every result URL behind an opaque `/goto?url=` redirect, so the Actor reads the page's embedded results array instead: every tweet that was an **organic result** gets its true SERP position (non-tweet results consume their slot, so ranks read 1, 4, 5, 6, 7, 9, 10 — not 1-7) and Google's own excerpt. Ids that appear only in a video/"posts" carousel carry no title or snippet triple and ship with `null` rank. Measured 22/22 ranks correct across 4 live queries. |
| `mediaCount` / `isRetweet` / `isReply` / `isQuote` | 100% | booleans and counts, never null |
| `possiblySensitive` | 77.4% | the embed API omits the flag on some tweets → `null` |
| **`media`** | **62.9%** | 39 of 62 tweets carried a photo, GIF or video |
| **`hashtags`** | **17.7%** | |
| **`urls`** | **17.7%** | external links only — t.co media links are stripped |
| `quotedTweetId` / `quotedTweetUrl` | 12.9% | |
| `mentions` | 8.1% | |
| **`retweets` / `quotes`** | **0% — not published by the embed API** | real numbers in account mode |
| **`authorFollowers` / `authorFollowing` / `authorTweetCount` / `authorBio` / `authorWebsite` / `authorCreatedAt`** | **0% unless `enrichAuthors` is on** | 100% on account-mode rows; `authorLocation` is always empty — X's syndication surface does not publish it |
| `inReplyTo*` / `retweeted*` | ~1% | Google mostly indexes original posts, not replies |
| `conversationId` | ~95% | the embed API does not publish `conversation_id_str`, so it is derived: a root tweet's conversation is itself (`conversation_id_str == id_str` on 498 of 498 root tweets measured across 7 real timelines), and a reply directly under a root roots at the parent. Deeper thread replies are left `null` rather than guessed. |

**Mixed run — 2 search terms + 2 accounts with `enrichAuthors: true` (60 rows: 41 account, 19
search).** `authorFollowers`, `authorFollowing`, `authorTweetCount`, `authorBio`, `authorCreatedAt`
**~85%** (enrichment misses some authors — a live `"series a" fintech` run resolved 8 of 10) ·
`authorWebsite` **96.7%** · `retweets` / `quotes` 68.3% (= every
account-mode row) · `media` 65.0% · `inReplyToHandle` 25.0% · `retweetedFromHandle` 20.0% (with
`includeRetweets: true`) · `authorLocation` **0% — X's syndication surface never publishes a
location, so this field is always null**.

**The headline that could mislead you: this is a "what people saw" product, not a firehose.**
Google's index decides which tweets search mode can see; the median tweet on the default run
carried ~860 likes because that is what gets indexed. For a complete recent record of one account,
use account mode.

**Every run prints its own transport counts in the log** — SERP pages usable / stub /
failed, tweets hydrated / gone / failed, timelines read / failed, authors enriched / missed, and
the number of rows your filters removed.

***

### How to run it

#### 1. Keyword, phrase or hashtag search (the usual choice)

```json
{
  "searchTerms": ["bitcoin etf", "\"climate change\"", "#nvidia"],
  "maxTweets": 300,
  "maxPagesPerTerm": 10
}
```

Each term becomes a `site:x.com …` Google query; each result page yields 2-10 tweet links
(7-10 per page on recent runs), and every link is hydrated from X's embed API. Pages are
fetched three at a time; the Actor stops a term when a page brings nothing new.

#### 2. A date window, a language or a recency window

```json
{
  "searchTerms": ["openai"],
  "since": "2026-08-01",
  "until": "2026-08-22",
  "language": "en"
}
```

`since` / `until` go to Google as `after:` / `before:`; `language` as `lr=lang_xx`; `recency`
(`day`, `week`, `month`, `year`) as Google's time filter when no window is set.

#### 3. Tweets from one account about one thing

```json
{ "searchTerms": ["from:OpenAI gpt", "from:elonmusk tesla since:2026-06-01"] }
```

`from:handle` becomes `site:x.com/handle`, so you get that account's indexed tweets matching the
words. Measured: `from:OpenAI gpt` returned 27 links over 3 pages, 18 delivered under the cap.

#### 4. An account's latest tweets, with full engagement counts

```json
{
  "twitterHandles": ["NASA", "https://x.com/OpenAI", "@sama"],
  "includeRetweets": false,
  "includeReplies": true,
  "maxTweets": 300
}
```

One request per account, newest first, **with retweets, quotes, replies and the author profile**.
Retweets are dropped by default. `includeRetweets: true` keeps them as rows with `isRetweet: true`,
`retweetedFromHandle` and the original tweet's text, counts and media. On those rows the author
block (`authorHandle`, `authorFollowers`, `authorBio` …) is the account you asked for — the one that
did the retweeting, matching the tweet `url` — and the original poster is in `retweetedFromHandle`. An unknown handle is reported in the log
and skipped; it never fails the run.

#### 5. Author enrichment for search results

```json
{
  "searchTerms": ["\"series a\" fintech"],
  "enrichAuthors": true,
  "maxAuthorsToEnrich": 100,
  "maxTweets": 200
}
```

Each unique author is looked up once (cached within the run) and their rows get followers, bio,
website, tweet count and account age (location is not published by this surface). Measured on a
live `"series a" fintech` run: 8 of 10 unique authors resolved, 10 of 12 rows enriched. The
`author-enriched` event bills **once per author found**, on top of the per-tweet price; misses and
authors past the cap are free.

#### Filters

`minLikes` · `onlyWithMedia` · `since` / `until` · `includeRetweets` / `includeReplies`
(account mode). Filters run **after** the tweet is fetched and **before** it is billed — a
filtered-out tweet costs you nothing.

#### Scheduling and integrations

Save the input as a **Task** and attach a **Schedule** (`0 */6 * * *` for a six-hourly brand
monitor); read the dataset over the REST API or any Apify integration (Zapier, Make, n8n,
webhooks, MCP). From the API:

```bash
curl -X POST "https://api.apify.com/v2/acts/scrapersdelight~x-tweet-search-scraper/runs?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"searchTerms":["\"#bitcoin\" etf"],"since":"2026-08-01","maxTweets":200}'
```

***

### Sample row

A real account-mode row from the mixed run, captured 2026-08-22 17:39 UTC.

```jsonc
{
  "id": "2085434712429052386",
  "url": "https://x.com/OpenAI/status/2085434712429052386",
  "text": "We're making better intelligence easier to access in ChatGPT for everyone:\n\n- GPT-5.6 Sol now powers both Instant and deep reasoning for Plus & Pro users, delivering more factual, focused responses.\n\n- Free & Go users get unlimited text chats with GPT-5.6 Luna starting tomorrow. https://t.co/JXhmj5GLTH",
  "createdAt": "2026-08-06T18:35:55.000Z",
  "lang": "en",

  "authorHandle": "OpenAI",
  "authorName": "OpenAI",
  "authorId": "4398626122",
  "authorVerified": true,
  "authorProfileImage": "https://pbs.twimg.com/profile_images/1885410181409820672/ztsaR0JW_400x400.jpg",
  "authorFollowers": 5121285,
  "authorFollowing": 4,
  "authorTweetCount": 2079,
  "authorBio": "OpenAI's mission is to ensure that artificial general intelligence benefits all of humanity. We're hiring: https://t.co/dJGr6LgzPA",
  "authorWebsite": "https://openai.com",
  "authorLocation": null,
  "authorCreatedAt": "2015-12-06T22:51:08.000Z",
  "authorEnriched": false,

  "likes": 21311,
  "replies": 1384,
  "retweets": 1919,
  "quotes": 1116,

  "isRetweet": false,
  "isReply": false,
  "isQuote": false,
  "inReplyToHandle": null,
  "inReplyToTweetId": null,
  "quotedTweetId": null,
  "quotedTweetUrl": null,
  "retweetedTweetId": null,
  "retweetedFromHandle": null,
  "conversationId": "2085434712429052386",

  "hashtags": [],
  "mentions": [],
  "urls": [],
  "media": [
    {
      "type": "video",
      "url": "https://pbs.twimg.com/amplify_video_thumb/2085433949489377280/img/4uce3Ngds5vczB6t.jpg",
      "videoUrl": "https://video.twimg.com/amplify_video/2085433949489377280/vid/avc1/1672x942/q_oTUaxOVBAP_2XY.mp4",
      "width": 1672,
      "height": 942
    }
  ],
  "mediaCount": 1,
  "possiblySensitive": false,

  "source": "account",
  "searchTerm": "from:OpenAI",
  "googleRank": null,
  "googleSnippet": null,
  "scrapedAt": "2026-08-22T17:39:28.287Z"
}
```

Fields people misread:

- `retweets: null` on a **search-mode** row means "not published by X's embed API", not zero.
  On an **account-mode** row it is a real count.
- `text` keeps X's trailing `https://t.co/…` media link exactly as X renders it; the actual image
  or MP4 is in `media[]`. `urls[]` holds only external links, already expanded.
- `authorEnriched: true` marks a search-mode row whose author block came from the enrichment
  lookup (and was billed once for that author). Account-mode rows carry the block for free and
  stay `false`.
- `googleRank` is the tweet's **true position on the Google result page that found it** (1-10 per
  page, counted over all organic results — so a page whose slots 2, 3 and 8 are profile pages
  yields ranks 1, 4, 5, 6, 7, 9, 10). It is null in account mode, and null for the handful of ids
  Google exposes only inside a video/"posts" carousel, which carries no snippet either.
  `googleSnippet` is Google's own excerpt for that result and follows the same rule.
- `conversationId` is the thread root. It is derived on search-mode rows (see the fill table) and
  is null on deep-thread replies, where the root is not in the payload.

***

### Input

Fields in the order the Console shows them.

| Field | Type | Default | What it does |
|---|---|---|---|
| **🔎 What to search** | | | |
| `searchTerms` | string list | `["bitcoin"]` | Words, `"phrases"`, `#hashtags`, `from:handle`, `since:` / `until:`, `lang:xx`, `min_faves:N`, `-word`. One search per line. |
| `twitterHandles` | string list | `[]` | Accounts (handle or profile URL) for their latest 20-100 tweets with full counts. |
| **🎚️ Filters** | | | |
| `since` / `until` | `YYYY-MM-DD` | blank | Date window — Google `after:` / `before:` in search mode, a filter in account mode. |
| `recency` | select | `any` | Past day / week / month / year (search mode, when no window is set). |
| `language` | string | blank | Two-letter code → `lr=lang_xx` (search mode). |
| `minLikes` | integer | `0` | Applied after fetch, before billing. |
| `onlyWithMedia` | boolean | `false` | Keep only tweets with a photo, GIF or video. |
| `includeRetweets` | boolean | `false` | Account mode. |
| `includeReplies` | boolean | `true` | Account mode. |
| **👤 Author enrichment** | | | |
| `enrichAuthors` | boolean | `false` | Search mode: look up each unique author once; bills `author-enriched` per author found. |
| `maxAuthorsToEnrich` | integer | `50` | Hard cap on enrichment events per run. |
| **💵 Limits & cost** | | | |
| `maxTweets` | integer | `100` | **Your hard cost cap.** 100 = $0.15, 1,000 = $1.50. |
| `maxTweetsPerTerm` | integer | `100` | Per-term / per-account ceiling. |
| `maxPagesPerTerm` | integer | `10` | Google pages per term (max 30); 7-10 tweets per page on recent runs. |
| **⚙️ Advanced** | | | |
| `proxyConfiguration` | proxy | Apify datacenter | For the X requests. Google always goes through Apify's GOOGLE\_SERP group. |

***

### Pricing

**$0.0015 per tweet returned — $1.50 per 1,000.** Charged on the `tweet-scraped` event. Optional
**$0.003 per author enriched** (`author-enriched`), only with `enrichAuthors: true` and only when the
profile is found. No monthly fee from this Actor.

| Run | Tweets | Cost |
|---|---|---|
| The shipped default (`bitcoin`, 10 pages) | ~70 | **$0.11** |
| Three terms at the default cap | 100 | **$0.15** |
| Five accounts' full latest timelines | ~150 | **$0.23** |
| A 1,000-tweet monitor | 1,000 | **$1.50** |
| 200 tweets + 100 authors enriched | 200 + 100 | **$0.60** |

- **You are charged for rows delivered.** A tweet Google listed but X no longer serves (deleted,
  protected, suspended) is skipped and never charged; so is anything your filters remove.
- **Duplicates are dropped before billing.** A tweet found by two terms, or by a term and an
  account, is one row and one charge; uniqueness is asserted on every run.
- **Rows are charged as they are pushed** (`Actor.pushData(items, 'tweet-scraped')`), so a run
  that hits your budget cap delivers whole rows and stops — never a half-billed dataset. The Actor
  reads the cap at startup and will not fetch tweets it cannot bill.
- Why not the $0.25-0.40 / 1,000 floor? Those prices are feed resellers' margins on a licensed
  search API. This Actor pays Google SERP proxy cost (roughly $0.0005 per tweet found) to stay
  logged-out and licence-free, and prices at the upper-middle of the lane for it.

***

### Honest limits

- **Search mode = Google's index of x.com.** Coverage is the popular and recent tweets Google has
  crawled, not every tweet ever posted. Narrow queries run out after 2-3 pages; broad ones keep
  going to the 30-page cap. Expect hours-to-days of indexing lag on brand-new tweets.
- **No `views` / impression counts anywhere** — neither public surface publishes them.
- **`retweets`, `quotes`, `isRetweet`, `retweetedTweetId` and `retweetedFromHandle` are null-or-
  false in search mode.** X's embed API does not publish them (verified on 78 live payloads, and
  on 8 alternate syndication endpoints) and a retweet permalink resolves to the original tweet, so
  search mode can never surface a retweet wrapper. Real values only in account mode.
- **Long-form tweets arrive truncated.** About 24% of search-mode rows are long-form posts whose
  `text` is X's ~280-character version, cut mid-sentence. The full body is not retrievable from any
  logged-out surface — X's embed payload carries only an opaque id for it. Account-mode rows are
  subject to the same X limit.
- **Account mode is X's embedded timeline: the newest 20 tweets for most accounts, up to 100 for
  high-volume ones, no pagination beyond that.** Protected accounts return nothing.
- **No replies-to-a-tweet / conversation threads.** That is a different function and belongs in a
  different Actor.
- **Google pages are billed by Apify's SERP proxy to the developer, not to you** — but it does
  mean `maxPagesPerTerm: 30` on a term that runs dry after page 3 costs nothing extra; the Actor
  stops as soon as a page brings no new tweet.
- **Two rate limits you might meet.** Google's SERP proxy occasionally returns a JavaScript stub
  (the Actor rotates the session and retries, up to 5 times per page — 0 of 34 pages needed it
  in measurement). X's timeline host 429'd 2 of 16 accounts on residential IPs and 0 of 16 on
  datacenter — the default is datacenter, with one residential retry if datacenter fails.
- **No login, no cookies, no CAPTCHA service, no account pool.** Everything here is the logged-out
  public surface X serves to embedded tweets and timelines. Nothing behind a login is touched, and
  the Actor will never ask you for a session cookie.

***

### How it works, and what it cost to make reliable

Two logged-out routes, one schema:

1. **Search**: `http://www.google.com/search?q=site:x.com+<terms>` through Apify's
   **GOOGLE\_SERP** proxy group — the only tier that returns a rendered Google page, and only over
   plain HTTP (it is a reverse proxy). Tweet ids are read from the result page by four
   tiers that union rather than fall through — the organic anchors where Google still exposes a
   direct URL, the page's embedded results array (the usual path now that Google wraps every link
   in a `/goto?url=` redirect; this is what carries `googleRank` and `googleSnippet`), stray
   anchors, and finally a bare `/status/` id scan so a carousel-only id is never dropped; each
   `/status/<id>` is hydrated from
   `https://cdn.syndication.twimg.com/tweet-result?id=<id>&token=<t>` — X's own embed API, the
   endpoint every embedded tweet on the web loads from.
2. **Account**: `https://syndication.twitter.com/srv/timeline-profile/screen-name/<handle>` — the
   page behind X's embedded timelines; its `__NEXT_DATA__` blob carries 20-100 full tweet objects
   with every engagement counter and the author record.

**Transport ladder, measured 2026-08-22 through Apify:**

| Route | Rung | Result |
|---|---|---|
| `x.com/search?q=` HTML | direct / datacenter / residential | 200, 277 KB JS shell, **0 tweets** on every rung |
| guest-token GraphQL `SearchTimeline` | all rungs | **404**; the logged-out client bundle (20 KB) contains no GraphQL operations at all |
| official `api.x.com/2/tweets/search/recent` | all rungs | **401** — licensed only |
| Bing / DuckDuckGo `site:x.com` | direct | 0 tweet links (Bing RSS ignores `site:`; DDG serves a challenge to datacenter IPs) |
| **Google `site:x.com` via GOOGLE\_SERP** | GOOGLE\_SERP (http) | **24/24 pages usable**, 138 distinct tweet ids over 8 queries × 3 pages, 2-10 links per page |
| **embed API `tweet-result`** | direct / datacenter / residential | **5/5, 5/5, 5/5** — JSON with text, author, likes, replies, media |
| **embedded timeline `timeline-profile`** | datacenter | **16/16** accounts, 644 tweets, 17.0 s total |
| embedded timeline `timeline-profile` | RESIDENTIAL + US | 14/16 (two HTTP 429), 36.9 s |
| embedded timeline `timeline-profile` | direct, plain `urllib` | 429 × 3 — it is fingerprint-sensitive; a Chrome-class client clears it |

So the default is the **cheap datacenter rung for X** and the **GOOGLE\_SERP group for Google**, and
you are never billed for residential IPs the Actor does not need.

Reliability is built in rather than hoped for: a wall-clock budget read from the run's own timeout
gates every request (a run that is about to hit its limit pushes what it has, sets a status message
and ends SUCCEEDED); every request timeout is clamped to the time left; an unhandled error ends
the run cleanly instead of FAILED; a bare `{}` input runs the documented sample; an empty result
ends with an explanation in the status message and bills nothing. Duplicates are removed on tweet
id before billing and uniqueness is asserted before the run may succeed.

`robots.txt` on x.com, syndication.twitter.com and cdn.syndication.twimg.com all carry a blanket
`User-agent: * / Disallow: /`; x.com's file allows Google and Bing to crawl `/i/api/`, hashtag and
cashtag searches and every `/status/` page, which is why Google has the index this Actor reads.
The Actor reads only what X serves logged-out to every embed on the web, paces itself, and never
touches an authenticated path.

***

### Duplicates — measured

Google re-serves some results across pages: over 24 measured pages, 17 of 155 tweet links were
repeats of an earlier page of the same query (**11.0%**), and a tweet about bitcoin can surface
under `bitcoin` and `#bitcoin` alike. The Actor keeps a **run-wide set keyed on tweet id**, drops
repeats before hydration (so they are never fetched, never billed), and then **asserts
uniqueness across everything it pushed** before the run is allowed to succeed. Both measured
runs: **0 duplicates in 62 and 60 rows.** Across runs, narrow with `since` — dedupe is per-run
by design.

***

### When a run fails

This Actor ends **SUCCEEDED with an explanation** on every transient condition and throws only
where output would be silently wrong:

- **0 rows** → a status message naming the cause: time budget, Google stub/blocked, no indexed
  tweets for the query, all accounts unreadable, or everything removed by your filters. Nothing
  billed.
- **Unknown handle / deleted tweet** → logged and skipped, never billed, never fatal.
- **Time budget** → pushes what it has, sets "Stopped early to stay inside this run's time
  limit — N tweets delivered and charged", ends SUCCEEDED.
- **Charge cap** → stops at the last whole row it can bill, says so in the status message.
- **Duplicate leak** → the uniqueness assertion fails the run (this is the one deliberate throw).

***

### Who buys this

- **Social-listening and brand-monitoring teams** — a scheduled `searchTerms` task with `since`
  set to yesterday gives you the indexed conversation about a brand, with likes and replies to
  rank it.
- **PR and comms** — `twitterHandles` of journalists, analysts and competitors: their latest
  posts with retweet and quote counts, every six hours, no account risk.
- **Lead-gen and GTM** — `"series a" fintech`, `"hiring" "head of growth"` with
  `enrichAuthors: true`: who is saying it, how many follow them and what website they link.
- **AI agents** — a keyword in, JSON tweets out, pay per tweet, no credentials to hold; the
  `text` field is ready for summarisation.
- **Researchers and analysts** — language- and date-windowed samples of the public conversation
  on a topic, with media URLs for multimodal work.

***

### FAQ

**Does this need an X account, login or cookies?**
No. Both surfaces are logged-out and public. The Actor never asks for, stores or uses a session
cookie, and there is no account pool behind it.

**Is this the full X search?**
No — see *Read this before you buy rows*. Search mode is Google's index of x.com, hydrated from
X. It is the tweets people saw, not every tweet. Account mode is complete for the newest 20-100
tweets of an account.

**Why are `retweets` and `quotes` null on some rows?**
Because X's embed API (search mode) publishes likes and reply counts only — checked on 78 live
payloads and on 8 alternate syndication endpoints, none of which carries them. Account-mode rows
have all four counts from X's timeline JSON.

**Can I get views?**
No. Neither public surface publishes impressions, so the field does not exist here.

**How fresh is search mode?**
As fresh as Google's index — hours to a few days for active accounts. `recency: "day"` asks
Google for the past 24 hours. For minute-level freshness on specific accounts use account mode.

**Do hashtags work?**
Yes. `#bitcoin` is sent to Google as `"#bitcoin"` (9 tweet links on page 1 in measurement, against
4 for the bare form).

**Do I need a residential proxy?**
No. Datacenter measured 16/16 timelines and 5/5 hydrations; residential measured worse on the
timeline host (two 429s). The Actor retries a failed timeline once through residential on its own.

**Two runs — will I get duplicates?**
Within a run, never: dedupe is on tweet id and asserted before success. Across runs, use `since`.

**Do I get charged for rows a filter removed, or for deleted tweets?**
No. Filters run before billing; a tweet X no longer serves is skipped and never charged.

**Will a run ever fail on an empty result?**
No. Zero rows ends SUCCEEDED with the cause in the status message and nothing billed. The only
deliberate failure is the uniqueness assertion.

***

### Legal & fair use

This Actor reads logged-out public pages and public embed endpoints that X serves to every website
embedding a tweet or a timeline, plus Google's public index of x.com. It does **not** log in, does
not use cookies or accounts, and collects nothing behind authentication. Tweets and author
profiles are public posts but can contain personal data; **you are responsible for complying with
X's Terms of Service, Google's Terms and applicable privacy law (GDPR, CCPA …) in how you use the
data.** Note that x.com and its syndication hosts publish a blanket `Disallow: /` in `robots.txt`
for unnamed crawlers.

X® and Twitter® are trademarks of their owner. This Actor is not affiliated with, endorsed by, or
connected to X Corp. or Google LLC.

***

### Feedback

Found a missing field or want a new filter? Open an issue on the **Issues** tab, and if the Actor
earns it, a review on the **Reviews** tab helps other buyers find it.

# Actor input Schema

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

One search per line. Plain words (bitcoin etf), exact phrases in quotes ("climate change"), hashtags (#bitcoin), X operators from:handle, since:YYYY-MM-DD, until:YYYY-MM-DD, lang:es and min\_faves:N, and Google's -word exclusion. Example: from:OpenAI gpt since:2026-08-01. SEARCH MODE READS GOOGLE'S INDEX OF X.COM, so it returns the tweets Google has indexed — typically the popular and recent ones — not the live firehose. Use the Account handles field for an account's complete latest 20-100 tweets.

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

X accounts whose latest tweets you want — a handle (NASA, @OpenAI) or a profile URL (https://x.com/nasa). Reads X's public embedded-timeline page: the newest 20 tweets for most accounts, up to 100 for high-volume ones, WITH retweet, reply and quote counts and the author's follower count, bio and website. Filtered by the date window, minimum likes and media filters below. Measured 16/16 accounts readable through the default datacenter proxy on 2026-08-22.

## `since` (type: `string`):

Only tweets posted on or after this day. Search mode: becomes Google after:YYYY-MM-DD (verified live 2026-08-22). Account mode: filters on the tweet's created date. Leave blank for no lower bound.

## `until` (type: `string`):

Only tweets posted on or before this day. Search mode: Google before:YYYY-MM-DD. Leave blank for no upper bound.

## `recency` (type: `string`):

Ask Google for tweets indexed in the past day / week / month / year. Ignored when a date window is set. Account mode is always newest-first.

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

Two-letter language code (es, de, ja, pt …). Restricts Google to pages in that language — lr=lang\_xx — which in practice returns tweets written in it. Verified: lang es returned Spanish-language accounts only. Leave blank for any language.

## `minLikes` (type: `integer`):

Drop tweets with fewer likes. Applied after the tweet is fetched, before billing — filtered rows cost nothing.

## `onlyWithMedia` (type: `boolean`):

Keep only tweets that carry at least one image, GIF or video. The media array on each row holds the direct pbs.twimg.com / video.twimg.com URLs.

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

Account timelines contain the account's retweets. OFF (default) drops them so you get the account's own posts. ON keeps them as rows with isRetweet = true, the original author in retweetedFromHandle and the original tweet's engagement counts.

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

ON (default) keeps the account's replies to other tweets (isReply = true, inReplyToHandle set). OFF drops them.

## `enrichAuthors` (type: `boolean`):

OFF by default so the first click stays cheap. ON adds one extra request per unique author (cached within the run) and bills the author-enriched event once per author found.

## `maxAuthorsToEnrich` (type: `integer`):

Hard cap on author-enriched events per run. Authors beyond the cap keep their basic fields and are not charged.

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

Stop after this many unique tweets across the whole run. At $0.0015 per tweet, 100 = $0.15, 1,000 = $1.50.

## `maxTweetsPerTerm` (type: `integer`):

Per-term ceiling so one broad term cannot eat the whole run budget. Account timelines publish at most ~100 tweets per account regardless.

## `maxPagesPerTerm` (type: `integer`):

Each Google page yields roughly 2-10 tweet links (measured 5.75 on average over 24 pages). 10 pages ≈ 50-70 tweets per term. Google stops serving new results after a few pages for narrow queries; the Actor stops on its own when a page brings nothing new.

## `proxyConfiguration` (type: `object`):

Default = Apify datacenter proxy, which measured BETTER than residential on X's syndication hosts (16/16 timelines vs 14/16 with two 429s on RESIDENTIAL+US, 2026-08-22). Hydration was 5/5 on direct, datacenter and residential alike. If a timeline 429s on datacenter the Actor retries it once through RESIDENTIAL automatically.

## Actor input object example

```json
{
  "searchTerms": [
    "bitcoin"
  ],
  "twitterHandles": [],
  "since": "",
  "until": "",
  "recency": "any",
  "language": "",
  "minLikes": 0,
  "onlyWithMedia": false,
  "includeRetweets": false,
  "includeReplies": true,
  "enrichAuthors": false,
  "maxAuthorsToEnrich": 50,
  "maxTweets": 100,
  "maxTweetsPerTerm": 100,
  "maxPagesPerTerm": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per unique tweet: id, url, text, createdAt, lang, author handle/name/id/verified (plus followers, bio, website on account-mode or enriched rows), likes, replies, retweets, quotes, reply/quote/retweet flags and targets, hashtags, mentions, expanded links, media (image/video URLs), possiblySensitive, source (search or account), the search term and Google rank that found it. Deduplicated run-wide on tweet id before billing.

# 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": [
        "bitcoin"
    ],
    "maxTweets": 100,
    "maxPagesPerTerm": 10,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/x-tweet-search-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": ["bitcoin"],
    "maxTweets": 100,
    "maxPagesPerTerm": 10,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/x-tweet-search-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": [
    "bitcoin"
  ],
  "maxTweets": 100,
  "maxPagesPerTerm": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/x-tweet-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/x-tweet-search-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/IAFHtmHm1eFwDp5A9/builds/thM9jvLahFB5o7ujb/openapi.json
