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

Scrape Twitter/X: tweets, user profiles, followers, hashtags, and trending topics.

- **URL**: https://apify.com/viralanalyzer/twitter-scraper.md
- **Developed by:** [viralanalyzer](https://apify.com/viralanalyzer) (community)
- **Categories:** Social media, News
- **Features:** Limited permissions
- **Stats:** 124 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $10.00 / 1,000 tweet 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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

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

## How to integrate an Actor?

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

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

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

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

# README

## Twitter/X Scraper 🐦

> 🔗 [View on Apify Store](https://apify.com/viralanalyzer/twitter-scraper) | 🇺🇸 English | [🇧🇷 Português](#português)

Scrape tweets, profiles, and engagement metrics from Twitter/X. Bring your own X API v2 Bearer Token for reliable access (recommended — X blocks anonymous timeline access), or use the anonymous fallback with configurable proxies.

***

### 🇺🇸 English

#### What does it do?

This actor extracts public tweets and engagement metrics from any Twitter/X profile or search query. Get real data for social media analysis, brand monitoring, and competitive intelligence.

#### Features

- **Profile scraping** — Get latest tweets from any public profile
- **Search scraping** — Find tweets matching any keyword or hashtag
- **Full metrics** — Views, likes, retweets, replies, quotes, bookmarks
- **Media extraction** — Each attachment as `{ type, url }`, from the anonymous strategies
- **Thread detection** — Identifies if a tweet is part of a thread
- **Author info** — Username, display name, follower count, verification status
- **Date filtering** — `dateFrom` / `dateTo` restrict results to a date range (UTC, both bounds inclusive), applied to each tweet's `created_at` before billing
- **Reply/Retweet filters** — Include or exclude replies and retweets
- **Replies mode** — pass `tweetUrls` (or `conversationIds`) and get one row per reply of a post, with the author and the badge
- **Newest first** — the batch is ordered by `created_at` and the pinned tweet is excluded before the cut, so `maxTweets: 1` returns the most recent tweet, not a pinned one from years ago
- **Verification badge** — `author.verifiedType` (`blue` | `business` | `government` | `none`), `author.isBlueVerified` and the legacy `author.verified`
- **No invented values** — a metric the source did not expose is `null`, never `0`/`false`

#### Capabilities & Limits

**Built here — measured 2026-09-23, read this before choosing a path:**

- **No token needed for the common case.** The actor reads the server-rendered x.com page, which embeds the profile's **5 newest tweets with view counts**, the pinned marker and reposts. Measured 2026-09-23: a profile whose syndication widget stopped in 2023 came back with its 2025 posts here, with `views` filled.
- **More than ~5 tweets** falls back to X's syndication widget (up to 100 per profile). That widget orders by **likes, not by date**, has **no view count** and hides retweets and replies — the actor sorts by date anyway and the run log warns when the newest tweet delivered is over 30 days old.
- **Replies** come from the post's own page (the first few, with author and badge). With an X API token the actor uses recent search instead, which covers the last 7 days.
- **With your X API token** the official API is used first: it paginates, sends your `dateFrom`/`dateTo` as `start_time`/`end_time` (so you do not pay for reads the filter would throw away), asks for media expansions and reads the pinned post id. X bills your own plan at **$0.005 per post read**, with minimums of 5 posts per timeline request and 10 per search.
- **Optional unblocking fallback.** If x.com refuses the ordinary request, and only then, the actor can fetch the same page through Bright Data Web Unlocker — billed **per request**, capped by `maxUnlockerRequests` (default 3). Leave `brightDataApiToken` empty to skip it.

What this actor can and cannot accept as a seed, based on the real input schema:

| Discover by | Supported | Notes |
|-------------|-----------|-------|
| Handle / username | ✅ Yes | `profiles` array (without `@`) |
| Hashtag | ⚠️ Partial | No dedicated field — pass it inside `searchQuery` (e.g. `#tech`); search mode needs the X API Bearer Token |
| Keyword / search | ⚠️ Partial | `searchQuery` works **only** with an X API v2 Bearer Token (X removed anonymous search); recent tweets only |
| Profile URL | ✅ Yes | `startUrls` or `seeds` + `seedType: "url"`; `x.com/home`, `/search` and `/i/...` are rejected and named in the log |
| Tweet URL (replies) | ✅ Yes | `tweetUrls` — returns the replies of that post, one row each |
| Subreddit | ❌ N/A | Not a Reddit actor |

**Output includes**: tweet text ✅, views ✅, likes ✅, retweets ✅, replies **count** ✅, quotes ✅, bookmarks ✅, hashtags ✅, media ✅, author (username, displayName, followers, verified) ✅, created_at (ISO 8601) ✅, language ✅, is_thread / is_reply / is_retweet ✅.

| Not in the output | | Why |
|---|---|---|
| Reply / comment **text** | ⚠️ replies mode | in timeline mode only the reply *count* is returned. Pass `tweetUrls` to get the text of each reply — **with an X API token**: without one, X answered the anonymous conversation request with HTTP 404 (measured 2026-09-23) and the run returns a diagnostic without charging |
| `thumbnail` on media items | ❌ | a media entry is `{ type, url }` — X's own media URL. No separate thumbnail or `:small` variant is emitted |
| Impressions on the syndication fallback | ⚠️ | the widget carries no view count, so `views` is `null` for tweets that came from it. The x.com page (used first) does carry views |

#### Input

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `profiles` | string\[] | — | Twitter/X usernames (with or without @). With no profile, search or tweet URL the run returns a diagnostic instead of scraping a default account |
| `tweetUrls` | string\[] | — | Replies mode: URLs (or ids) of the posts whose replies you want |
| `includePinned` | boolean | false | Keep the pinned tweet in the results (it is excluded by default because it is often years old) |
| `brightDataApiToken` | string | — | Optional unblocking fallback, billed per request. Empty = never used |
| `maxUnlockerRequests` | integer | 3 | Hard cap on that paid fallback per run |
| `searchQuery` | string | — | Search term (alternative to profiles) |
| `maxTweets` | integer | 20 | Max tweets per profile/search (1-100) |
| `includeReplies` | boolean | false | Include reply tweets |
| `includeRetweets` | boolean | false | Include retweets |
| `dateFrom` | string | — | Inclusive lower bound on `created_at`. `YYYY-MM-DD` (= `00:00:00.000Z` UTC) or full ISO-8601 |
| `dateTo` | string | — | Inclusive upper bound on `created_at`. `YYYY-MM-DD` (= `23:59:59.999Z` UTC, whole day included) or full ISO-8601 |

#### Date range (`dateFrom` / `dateTo`) - exact behavior

Both fields are optional and independent; leaving one empty leaves that side unbounded, and leaving both empty disables the filter entirely.

- **Where it runs.** With an X API token the bounds are sent to the API as `start_time`/`end_time`, so you are not billed for posts the filter would discard (the API accepts dates from 2010-11-06 on the timeline; recent search only covers the last 7 days). The anonymous strategies have no date parameter, so there the comparison happens locally on each tweet's `created_at` — a narrow or old range can return **fewer items than `maxTweets`**, or nothing at all.
- **Inclusive, UTC.** A tweet is kept when `dateFrom <= created_at <= dateTo`. `YYYY-MM-DD` in `dateFrom` means `00:00:00.000Z` of that day; in `dateTo` it means `23:59:59.999Z`, so `dateFrom = dateTo = 2026-01-20` returns that whole day. A full ISO-8601 timestamp is used exactly as given.
- **Billing.** The filter runs **before** the tweet is stored and before the Pay Per Event counter is incremented. **You are never charged for a tweet the date filter discards.**
- **Tweets with no date.** A few anonymous fallback paths return a tweet whose `created_at` is `null`. While any bound is set, such a tweet is **dropped** - its membership in the range cannot be verified, so it is neither returned nor charged. The run log reports how many were dropped for this reason.
- **Invalid input.** An unreadable `dateFrom` / `dateTo`, or a range where `dateFrom` is after `dateTo`, **stops the run** with a labeled `_dataQuality: "diagnostic"` item explaining the problem, and charges nothing. The filter is never silently ignored.
- **Nothing in range.** If tweets were found but all fell outside the range, the run succeeds with a diagnostic item stating how many were discarded - again with no charge.

#### Output Example

```json
{
  "tweet_id": "1891234567890123456",
  "url": "https://x.com/elikifreitasdev/status/1891234567890123456",
  "text": "🚀 Launched the new version! Performance 3x better #tech #automation",
  "author": {
    "username": "elikifreitasdev",
    "displayName": "Eliki Freitas | Dev",
    "followers": 12500,
    "verified": false,
    "isBlueVerified": true,
    "verifiedType": "blue"
  },
  "created_at": "2026-01-20T14:30:00.000Z",
  "views": 45200,
  "likes": 892,
  "retweets": 156,
  "replies": 43,
  "quotes": 12,
  "bookmarks": 234,
  "hashtags": ["tech", "automation"],
  "media": [
    {
      "type": "image",
      "url": "https://pbs.twimg.com/media/example.jpg"
    }
  ],
  "is_thread": false,
  "is_reply": false,
  "is_retweet": false,
  "is_pinned": false,
  "conversation_id": "1891234567890123456",
  "in_reply_to_id": null,
  "language": "en"
}
```

#### Use Cases

1. **Brand Monitoring** — Track mentions and sentiment about your brand
2. **Competitor Analysis** — Monitor competitor tweet performance
3. **Influencer Research** — Evaluate engagement rates before partnerships
4. **Trend Detection** — Discover viral topics and hashtags
5. **Content Strategy** — Analyze what tweet formats get the most engagement

#### FAQ

**Does it require a Twitter API key?**
Recommended. X blocks anonymous (guest) access to timelines, so for reliable results provide your own `twitterBearerToken` (X API v2 — your own plan/quota at https://developer.x.com). **Search/keyword mode (`searchQuery`) requires the Bearer Token** (X removed anonymous search). Without a token, the actor tries anonymous strategies (subject to blocking) and, if nothing is extracted, returns a diagnostic guide without charging PPE.

**Why did `dateFrom` / `dateTo` return fewer tweets than `maxTweets`?**
Because the range is applied locally, to the tweets the actor was able to fetch (the most recent ones a strategy exposes), not as a remote query parameter. Tweets outside the range — and, while a bound is set, tweets with no readable `created_at` — are dropped before being stored, and are **not charged**. See "Date range — exact behavior" above.

**Why did I get an old tweet / when is `views` null?**
The syndication widget returns a profile's tweets **ordered by engagement, not by date**, and the old build pushed whatever came first — that is why a 2022 post was returned for `maxTweets: 1`. The actor now reads the x.com page first (newest tweets, with view counts), orders everything by `created_at` and drops the pinned tweet before cutting. `views` is `null` only for tweets that came from the syndication fallback, which has no impression count — never a fabricated `0`.

**What do `verified`, `isBlueVerified` and `verifiedType` mean?**
`verifiedType` is X's own value: `blue`, `business`, `government` or `none`. `isBlueVerified` is true only for the blue badge. `verified` is the legacy flag. Any of them is `null` when the source did not say — `null` means unknown, `false` means the source said no.

**How do I read the replies of a post?**
Set `tweetUrls: ["https://x.com/nasa/status/123..."]`. Without a token the actor reads the post's own page and returns **the first replies X renders there** (3 in the measurements, each with author and badge) — enough to sample a conversation, not to export all of it. With an X API token it uses recent search with the `conversation_id` operator instead, which returns the whole thread for the **last 7 days** (older threads need X's full-archive access).

**Can it scrape protected/private accounts?**
No. Only public profiles and public tweets are accessible.

**What about rate limits?**
The actor handles rate limiting automatically with built-in delays and retries.

**How many tweets can I get per run?**
Up to 100 tweets per profile or search query per run.

***

### 🇧🇷 Português

#### O que faz?

Este actor extrai tweets públicos e métricas de engajamento de qualquer perfil ou busca no Twitter/X. Dados reais para análise de redes sociais, monitoramento de marca e inteligência competitiva.

#### Funcionalidades

- **Scraping de perfil** — Últimos tweets de qualquer perfil público
- **Scraping de busca** — Encontre tweets por palavra-chave ou hashtag
- **Métricas completas** — Visualizações, curtidas, retweets, respostas, citações, bookmarks
- **Extração de mídia** — Cada anexo como `{ type, url }`, vindo das estratégias anônimas
- **Detecção de thread** — Identifica se o tweet faz parte de uma thread
- **Info do autor** — Username, nome, seguidores, verificação
- **Filtro por data** — `dateFrom` / `dateTo` restringem os resultados a um período (UTC, ambos os limites inclusivos), aplicados ao `created_at` de cada tweet antes da cobrança
- **Filtros de resposta/retweet** — Inclua ou exclua respostas e retweets
- **Modo replies** — informe `tweetUrls` (ou `conversationIds`) e receba uma linha por resposta do post, com o autor e o selo
- **Mais recente primeiro** — o lote é ordenado por `created_at` e o tweet fixado é excluído antes do corte, então `maxTweets: 1` devolve o tweet mais recente, não um fixado de anos atrás
- **Selo de verificação** — `author.verifiedType` (`blue` | `business` | `government` | `none`), `author.isBlueVerified` e o `author.verified` legado
- **Sem valor inventado** — métrica que a fonte não expôs vem `null`, nunca `0`/`false`

#### Capacidades e Limites

**Medido em 2026-09-23, leia antes de escolher o caminho:**

- **Sem token no caso comum.** O actor lê a página do x.com renderizada no servidor, que já traz os **5 tweets mais recentes com contagem de visualizações**, o marcador de fixado e as repostagens. Medido em 2026-09-23: um perfil cujo widget de syndication parava em 2023 voltou aqui com posts de 2025, com `views` preenchido.
- **Acima de ~5 tweets** entra o widget de syndication (até 100 por perfil). Esse widget ordena por **curtidas, não por data**, **não tem visualizações** e esconde retweets e respostas — o actor ordena por data mesmo assim, e o log avisa quando o tweet mais recente tem mais de 30 dias.
- **Respostas** vêm da página do próprio post (as primeiras, com autor e selo). Com token da API do X o actor usa a busca recente, que cobre os últimos 7 dias.
- **Com o seu token da API do X** a API oficial vem primeiro: pagina, envia `dateFrom`/`dateTo` como `start_time`/`end_time`, pede as expansões de mídia e lê o id do fixado. O X cobra do seu plano **US$ 0,005 por post lido**, com mínimos de 5 posts por requisição de timeline e 10 por busca.
- **Fallback opcional de desbloqueio.** Se o x.com recusar a requisição comum, e só nesse caso, o actor pode buscar a mesma página pelo Web Unlocker da Bright Data — cobrado **por requisição**, limitado por `maxUnlockerRequests` (padrão 3). Deixe `brightDataApiToken` vazio para não usar.

O que este actor aceita ou não como semente, conforme o input schema real:

| Descobre por | Suportado | Observações |
|--------------|-----------|-------------|
| Handle / username | ✅ Sim | Array `profiles` (sem `@`) |
| Hashtag | ⚠️ Parcial | Sem campo dedicado — passe dentro de `searchQuery` (ex.: `#tech`); o modo de busca exige o Bearer Token da API do X |
| Palavra-chave / busca | ⚠️ Parcial | `searchQuery` funciona **apenas** com Bearer Token da API X v2 (o X removeu a busca anônima); somente tweets recentes |
| URL de perfil | ✅ Sim | `startUrls` ou `seeds` + `seedType: "url"`; `x.com/home`, `/search` e `/i/...` são rejeitados e nomeados no log |
| URL de tweet (replies) | ✅ Sim | `tweetUrls` — devolve as respostas daquele post, uma por linha |
| Subreddit | ❌ N/A | Não é um actor de Reddit |

**A saída inclui**: texto do tweet ✅, visualizações ✅, curtidas ✅, retweets ✅, **contagem** de respostas ✅, citações ✅, bookmarks ✅, hashtags ✅, mídia ✅, autor (username, displayName, seguidores, verificado) ✅, created_at (ISO 8601) ✅, idioma ✅, is_thread / is_reply / is_retweet ✅.

| Não sai na saída | | Motivo |
|---|---|---|
| **TEXTO** das respostas/comentários | ⚠️ modo replies | no modo timeline só a *contagem* é retornada. Informe `tweetUrls` para obter o texto de cada resposta — **com token da API do X**: sem token, o X respondeu 404 à consulta anônima de conversa (medido em 2026-09-23) e o run devolve diagnóstico sem cobrar |
| `thumbnail` nos itens de mídia | ❌ | cada anexo é `{ type, url }` — a própria URL de mídia do X. Não há thumbnail separado nem variante `:small` |
| Visualizações no fallback de syndication | ⚠️ | o widget não traz contagem de views, então `views` vem `null` nos tweets vindos dele. A página do x.com, usada primeiro, traz views |

#### Entrada

| Campo | Tipo | Padrão | Descrição |
|-------|------|--------|-----------|
| `profiles` | string\[] | — | Usernames do Twitter/X (com ou sem @). Sem perfil, busca ou URL de tweet, o run devolve diagnóstico em vez de raspar uma conta padrão |
| `tweetUrls` | string\[] | — | Modo replies: URLs (ou ids) dos posts cujas respostas você quer |
| `includePinned` | boolean | false | Mantém o tweet fixado nos resultados (por padrão ele sai, porque costuma ser antigo) |
| `brightDataApiToken` | string | — | Fallback opcional de desbloqueio, cobrado por requisição. Vazio = nunca usado |
| `maxUnlockerRequests` | inteiro | 3 | Teto do fallback pago por execução |
| `searchQuery` | string | — | Termo de busca (alternativa aos perfis) |
| `maxTweets` | integer | 20 | Máx tweets por perfil/busca (1-100) |
| `includeReplies` | boolean | false | Incluir tweets de resposta |
| `includeRetweets` | boolean | false | Incluir retweets |
| `dateFrom` | string | — | Limite inferior inclusivo sobre `created_at`. `YYYY-MM-DD` (= `00:00:00.000Z` UTC) ou ISO-8601 completo |
| `dateTo` | string | — | Limite superior inclusivo sobre `created_at`. `YYYY-MM-DD` (= `23:59:59.999Z` UTC, dia inteiro incluído) ou ISO-8601 completo |

#### Intervalo de datas (`dateFrom` / `dateTo`) - comportamento exato

Os dois campos são opcionais e independentes: deixar um vazio deixa aquele lado sem limite, e deixar os dois vazios desliga o filtro.

- **Onde roda.** Com token da API do X os limites vão para a API como `start_time`/`end_time`, então você não paga por posts que o filtro descartaria (a API aceita datas a partir de 2010-11-06 na timeline; a busca recente cobre só os últimos 7 dias). As estratégias anônimas não têm parâmetro de data, então ali a comparação é local sobre o `created_at` — um intervalo estreito ou antigo pode devolver **menos itens que `maxTweets`**, ou nenhum.
- **Inclusivo, em UTC.** O tweet é mantido quando `dateFrom <= created_at <= dateTo`. `YYYY-MM-DD` em `dateFrom` significa `00:00:00.000Z` daquele dia; em `dateTo` significa `23:59:59.999Z`, então `dateFrom = dateTo = 2026-01-20` devolve o dia inteiro. Um timestamp ISO-8601 completo é usado exatamente como informado.
- **Cobrança.** O filtro roda **antes** de o tweet ser gravado e antes de o contador do Pay Per Event ser incrementado. **Você nunca é cobrado por um tweet descartado pelo filtro de data.**
- **Tweets sem data.** Alguns caminhos anônimos de fallback devolvem tweet com `created_at` igual a `null`. Com qualquer limite ativo, esse tweet é **descartado** - não dá para verificar se ele pertence ao intervalo, então ele não é devolvido nem cobrado. O log do run informa quantos caíram por esse motivo.
- **Entrada inválida.** `dateFrom` / `dateTo` ilegível, ou intervalo em que `dateFrom` é posterior a `dateTo`, **interrompe o run** com um item `_dataQuality: "diagnostic"` explicando o problema, sem cobrar nada. O filtro nunca é ignorado em silêncio.
- **Nada no intervalo.** Se tweets foram encontrados mas todos ficaram fora do intervalo, o run termina com sucesso e um item de diagnóstico dizendo quantos foram descartados - também sem cobrança.

#### Exemplo de Saída

```json
{
  "tweet_id": "1891234567890123456",
  "url": "https://x.com/elikifreitasdev/status/1891234567890123456",
  "text": "🚀 Lançamos a nova versão! Performance 3x melhor #tech #automation",
  "author": {
    "username": "elikifreitasdev",
    "displayName": "Eliki Freitas | Dev",
    "followers": 12500,
    "verified": false,
    "isBlueVerified": true,
    "verifiedType": "blue"
  },
  "created_at": "2026-01-20T14:30:00.000Z",
  "views": 45200,
  "likes": 892,
  "retweets": 156,
  "replies": 43,
  "quotes": 12,
  "bookmarks": 234,
  "hashtags": ["tech", "automation"],
  "media": [
    {
      "type": "image",
      "url": "https://pbs.twimg.com/media/example.jpg"
    }
  ],
  "is_thread": false,
  "is_reply": false,
  "is_retweet": false,
  "is_pinned": false,
  "conversation_id": "1891234567890123456",
  "in_reply_to_id": null,
  "language": "pt"
}
```

#### Casos de Uso

1. **Monitoramento de Marca** — Acompanhe menções e sentimento sobre sua marca
2. **Análise de Concorrência** — Monitore performance de tweets de concorrentes
3. **Pesquisa de Influenciadores** — Avalie taxas de engajamento antes de parcerias
4. **Detecção de Tendências** — Descubra tópicos virais e hashtags
5. **Estratégia de Conteúdo** — Analise quais formatos de tweet geram mais engajamento

#### FAQ

**Precisa de chave de API do Twitter?**
Recomendado. O X bloqueia acesso anônimo a timelines; informe seu `twitterBearerToken` (X API v2, plano/quota próprios — https://developer.x.com) para extração confiável. Sem token, o actor tenta estratégias anônimas (sujeitas a bloqueio) e, se nada for extraído, devolve um guia de diagnóstico sem cobrar PPE.

**Por que `dateFrom` / `dateTo` devolveram menos tweets que `maxTweets`?**
Porque o intervalo é aplicado localmente, sobre os tweets que o actor conseguiu buscar (os mais recentes que a estratégia expõe), e não como parâmetro remoto de consulta. Tweets fora do intervalo — e, com um limite ativo, tweets sem `created_at` legível — são descartados antes de serem gravados, e **não são cobrados**. Veja "Intervalo de datas — comportamento exato" acima.

**Por que veio um tweet antigo / quando `views` vem null?**
O widget de syndication devolve os tweets **ordenados por engajamento, não por data**, e a versão antiga gravava o primeiro que viesse — foi por isso que um post de 2022 saiu com `maxTweets: 1`. Agora o actor lê primeiro a página do x.com (mais recentes, com visualizações), ordena tudo por `created_at` e tira o fixado antes do corte. `views` só vem `null` nos tweets vindos do fallback de syndication, que não tem esse número — nunca um `0` inventado.

**O que significam `verified`, `isBlueVerified` e `verifiedType`?**
`verifiedType` é o valor do próprio X: `blue`, `business`, `government` ou `none`. `isBlueVerified` só é verdadeiro no selo azul. `verified` é a flag legada. Qualquer um deles vem `null` quando a fonte não disse — `null` é desconhecido, `false` é a fonte dizendo que não.

**Como leio as respostas de um post?**
Use `tweetUrls: ["https://x.com/nasa/status/123..."]`. Sem token o actor lê a página do próprio post e devolve **as primeiras respostas que o X renderiza ali** (3 nas medições, cada uma com autor e selo) — dá para amostrar uma conversa, não para exportá-la inteira. Com token da API do X ele usa a busca recente com o operador `conversation_id`, que devolve a thread dos **últimos 7 dias** (mais antigas exigem o acesso full-archive do X).

**Pode scraper contas protegidas/privadas?**
Não. Apenas perfis públicos e tweets públicos são acessíveis.

**E sobre limites de requisição?**
O actor gerencia rate limiting automaticamente com delays e retentativas.

**Quantos tweets posso obter por execução?**
Até 100 tweets por perfil ou busca por execução.

***

### 💰 Pricing

This actor uses **Pay Per Event (PPE)** pricing. The rate depends on your Apify plan tier:

| Plan tier | Per tweet | Per 1,000 tweets |
|-----------|-----------|------------------|
| Free      | $0.02     | $20.00 |
| Bronze    | $0.0175   | $17.50 |
| Silver    | $0.015    | $15.00 |
| Gold      | $0.01     | $10.00 |
| Platinum  | $0.0075   | $7.50  |
| Diamond   | $0.005    | $5.00  |

**You are never charged for a run that returned nothing.** When X blocks the request and no
tweet is extracted, the actor returns a labeled `_dataQuality: "diagnostic"` setup guide and
charges no PPE event. The same holds for the `dateFrom` / `dateTo` filter: tweets discarded
because they fall outside the requested range — or because they carry no readable date while a
bound is set — are dropped before the Pay Per Event counter, so they are never billed.

### 🔗 Related Actors

- [Instagram Reels & Posts Scraper](https://apify.com/viralanalyzer/instagram-reels-scraper)
- [TikTok Viral Scanner](https://apify.com/viralanalyzer/tiktok-viral-scanner)
- [Reddit Scraper](https://apify.com/viralanalyzer/reddit-scraper)
- [YouTube Fast Scraper](https://apify.com/viralanalyzer/youtube-fast-scraper)

***

### ✅ Available / Disponível — cross-actor upgrades

> The items below were **PLANNED** in earlier versions and are now **implemented and live** in this actor. They are **additive and backward-compatible**: when the new input fields are omitted and `normalizeOutput` is `false`, the output is byte-for-byte identical to before — paying users are unaffected.
>
> Os itens abaixo eram **PLANEJADOS** em versões anteriores e agora estão **implementados e disponíveis** neste actor. São **aditivos e retrocompatíveis**: quando os novos campos de input são omitidos e `normalizeOutput` é `false`, a saída é byte-a-byte idêntica à anterior — usuários pagantes não são afetados.

**EN**

- **Unified input vocabulary** (#1) ✅ — `maxResults` (alias of `maxTweets`), `seeds` + `seedType` (`handle` | `url` | `keyword`) are accepted as a cross-actor unified seed list. The existing `profiles` / `searchQuery` / `maxTweets` fields keep working exactly as before; the new ones are optional aliases.
- **Normalized `_normalized` output block** (#2) ✅ — set the optional input `normalizeOutput: true` to attach a cross-platform block to each item: `{ platform, url, author, text, views, likes, comments, shares, publishedAtISO, lang, hashtags[], engagementVelocity }`, alongside the existing raw fields. For Twitter/X, `comments` maps from `replies`, `shares` from `retweets + quotes`, `views` from impressions. Default `false`.
- **ISO date + `engagementVelocity`** (#6) ✅ — inside `_normalized`, `publishedAtISO` is the tweet date in ISO-8601 UTC, and `engagementVelocity` = `(likes + comments + shares) / hours_since_published` (or `null` when no date is available).
- **Quality flag `_dataQuality`** (#7) ✅ — successful items carry `_dataQuality: "full"` (when `normalizeOutput` is on); the recoverable no-results setup guide carries `_dataQuality: "diagnostic"` and is **not** charged via PPE.

**PT**

- **Vocabulário de input unificado** (#1) ✅ — `maxResults` (alias de `maxTweets`), `seeds` + `seedType` (`handle` | `url` | `keyword`) são aceitos como lista de sementes unificada entre actors. Os campos `profiles` / `searchQuery` / `maxTweets` continuam funcionando exatamente como antes; os novos são aliases opcionais.
- **Bloco de saída normalizado `_normalized`** (#2) ✅ — defina o input opcional `normalizeOutput: true` para anexar a cada item um bloco cross-plataforma: `{ platform, url, author, text, views, likes, comments, shares, publishedAtISO, lang, hashtags[], engagementVelocity }`, ao lado dos campos crus. No Twitter/X, `comments` vem de `replies`, `shares` de `retweets + quotes`, `views` das impressões. Padrão `false`.
- **Data ISO + `engagementVelocity`** (#6) ✅ — dentro de `_normalized`, `publishedAtISO` é a data do tweet em ISO-8601 UTC, e `engagementVelocity` = `(likes + comments + shares) / horas_desde_publicação` (ou `null` quando não há data).
- **Flag de qualidade `_dataQuality`** (#7) ✅ — itens bem-sucedidos carregam `_dataQuality: "full"` (quando `normalizeOutput` está ligado); o guia de diagnóstico recuperável (sem resultados) carrega `_dataQuality: "diagnostic"` e **não** é cobrado via PPE.

***

### 🛣️ Roadmap / Próximas melhorias (planned)

> ⚠️ The items below are **PLANNED**, not yet available. They are not current features — do not rely on them yet. Source: internal social-scrapers improvements handoff (2026-06-19).
>
> ⚠️ Os itens abaixo são **PLANEJADOS**, ainda não disponíveis. Não são funcionalidades atuais — não dependa deles ainda. Fonte: handoff interno de melhorias dos scrapers sociais (2026-06-19).

**EN**

- (nothing pending here — the structured `_blockReason` shipped in v1.2: the diagnostic item now carries `_blockReason: "USER_FILTER_EMPTY"` when your own filters emptied the result.)

**PT**

- (nada pendente aqui — o `_blockReason` estruturado entrou na v1.2: o item de diagnóstico agora traz `_blockReason: "USER_FILTER_EMPTY"` quando os seus próprios filtros esvaziaram o resultado.)

***

### 📝 Changelog

- **v1.3.0** (2026-09-23) — New primary source: the server-rendered x.com page, read without executing any of its JavaScript. It brings what the syndication widget never had — correct dates (a profile stuck in 2023 there returns its 2025 posts here), **view counts**, the pinned marker, reposts and the first replies of a post. Optional Bright Data Web Unlocker fallback, billed per request and capped per run (`maxUnlockerRequests`, default 3). The dead guest-token strategies were removed along with the hardcoded web-client bearer.
- **v1.2.0** (2026-09-23) — Fixed: the anonymous source returns tweets ordered by engagement, so `maxTweets: 1` could return a post from years ago; the batch is now ordered by date and the pinned tweet is excluded by default (`includePinned` keeps it). Metrics the source does not expose are `null` instead of `0`/`false`, and the author now carries `isBlueVerified` and `verifiedType`. New replies mode (`tweetUrls` / `conversationIds`). On the X API path: date bounds are sent as `start_time`/`end_time`, results paginate, media expansions are requested, 429 is retried using `x-rate-limit-reset` and every request has a timeout. Profile URLs like `/home` and `/i/...` are rejected instead of being scraped as a handle, and a run with nothing to scrape returns a diagnostic instead of falling back to a default account.
- **v1.1.0** (2026-08-27) — `dateFrom` / `dateTo` are now honored: the range is enforced in memory against each tweet's `created_at` (UTC, both bounds inclusive) before the tweet is stored and before Pay Per Event, so filtered-out tweets are never charged. Tweets with no readable date are dropped while a bound is set, and an invalid bound stops the run with a diagnostic item instead of being silently ignored.
- **v1.0.0** (2026-02-12) — Initial release: profile scraping, search, full engagement metrics, thread detection, media extraction

# Actor input Schema

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

List of Twitter/X usernames to scrape (e.g. \['nasa', 'elonmusk']), with or without @. An entry that is not a valid handle (1-15 letters, digits or \_) or a profile URL is ignored and named in the log. With no profile, search query or tweet URL, the run returns a diagnostic item instead of scraping a default account.

## `tweetUrls` (type: `array`):

Read the REPLIES of these posts, one item per reply, each with the author and the badge. Accepts a full URL (https://x.com/nasa/status/1234567890) or a bare post id. When this is set, the actor ignores profiles/searchQuery and runs in replies mode. Without an X API token the actor tries the anonymous path and, when X refuses it, returns a diagnostic item without charging. With a token, replies come from recent search (conversation_id operator), which only covers the last 7 days.

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

Same as Tweet URLs, but taking the conversation id directly. Use either one.

## `searchQuery` (type: `string`):

Search term across Twitter/X (alternative to profiles). Leave empty to use profiles list.

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

Maximum number of items to return per profile, search or conversation. Tweets are ordered newest-first before the cut. Note on the official X API: its timeline endpoint reads a minimum of 5 posts per request and search a minimum of 10, so asking for 1 still bills you for 5 (or 10) post reads at $0.005 each.

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

Cross-actor alias for maxTweets. When set, it overrides maxTweets.

## `seeds` (type: `array`):

Cross-actor unified seed list. Optional. Combined with 'seedType' to choose how each entry is interpreted. If omitted/empty, the actor uses 'profiles' / 'searchQuery' exactly as before. seedType=handle → usernames (without @); seedType=url → twitter.com/x.com profile or tweet URLs (the username is extracted); seedType=keyword → search terms (requires the X API Bearer Token, like searchQuery).

## `seedType` (type: `string`):

How to interpret the 'seeds' entries. Only applies when 'seeds' is non-empty. 'handle' = Twitter/X usernames; 'url' = profile/tweet URLs (username extracted); 'keyword' = search terms (requires the X API Bearer Token).

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

Include reply tweets in results

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

Include retweets in results

## `includePinned` (type: `boolean`):

A profile's pinned tweet can be years old. By default it is excluded, so "Max Tweets: 1" returns the most RECENT tweet instead of the pinned one. Set true to keep it — it is then ordered by date like any other tweet and carries is_pinned: true.

## `dateFrom` (type: `string`):

Lower bound, inclusive. YYYY-MM-DD (00:00:00.000Z of that day, UTC) or a full ISO-8601 timestamp. With an X API token the bound is sent to the API (start_time), so you do not pay for reads the filter would discard; the anonymous strategies have no date parameter and are filtered in memory. A tweet the filter drops is never pushed and never charged. An unreadable value stops the run with a diagnostic and no charge.

## `dateTo` (type: `string`):

Upper bound, inclusive. YYYY-MM-DD (23:59:59.999Z of that day, UTC) or a full ISO-8601 timestamp. Sent to the API as end_time when a token is provided. Same billing rule as Date From.

## `twitterBearerToken` (type: `string`):

Official X (Twitter) API v2 Bearer Token. When provided, the actor calls the official API first — the only reliable method, since X blocks anonymous (guest) access to timelines. Get yours at https://developer.x.com (your own X API plan/quota applies).

## `brightDataApiToken` (type: `string`):

Optional. Used ONLY when x.com refuses the ordinary request: the actor then fetches the same page through Bright Data Web Unlocker, which is billed per REQUEST (not per record). Leave empty to skip the paid fallback entirely — the actor still tries the free paths and returns a diagnostic if they are blocked.

## `brightDataZone` (type: `string`):

Unlocker zone name for the token above. Defaults to the zone configured on the actor.

## `maxUnlockerRequests` (type: `integer`):

Hard cap on the paid fallback, so a blocked target cannot run up a bill. Default 3, maximum 10.

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

Proxy used by the anonymous scraping strategies. Default = Apify datacenter proxy (low cost). If runs return only a diagnostic item, enable RESIDENTIAL proxies here — note that residential traffic is billed as platform usage.

## `normalizeOutput` (type: `boolean`):

When true, each item additionally carries a cross-platform '\_normalized' block { platform, url, author, text, views, likes, comments, shares, publishedAtISO, lang, hashtags\[], engagementVelocity } alongside the existing raw fields. Default false — output is byte-for-byte identical to before.

## Actor input object example

```json
{
  "profiles": [
    "nasa"
  ],
  "maxTweets": 20,
  "seedType": "handle",
  "includeReplies": false,
  "includeRetweets": false,
  "includePinned": false,
  "maxUnlockerRequests": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "normalizeOutput": false
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset containing all scraped results. Each item follows the dataset schema.

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "profiles": [
        "nasa"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

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

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = {
    "profiles": ["nasa"],
    "proxyConfiguration": { "useApifyProxy": True },
}

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

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

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

```

## CLI example

```bash
echo '{
  "profiles": [
    "nasa"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call viralanalyzer/twitter-scraper --silent --output-dataset

```

## MCP server setup

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