# X (Twitter) Profile & Tweets Scraper — No Login, No Cookies (`nick.cheng/x-twitter-profile-tweets-scraper`) Actor

Scrape X (Twitter) profiles and tweets by handle, URL, or user ID. No login, no cookies, no proxies. Charged only for results actually returned.

- **URL**: https://apify.com/nick.cheng/x-twitter-profile-tweets-scraper.md
- **Developed by:** [Nicholas Cheng](https://apify.com/nick.cheng) (community)
- **Categories:** Social media, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 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?

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) Profile & Tweets Scraper

**Give it a handle, get back that account's profile and tweets as clean JSON.** No login, no cookies, no session tokens, no proxy configuration. Point it at `nasa`, `https://x.com/openai`, or a numeric user ID, and it returns structured records you can drop straight into a database or feed to an agent.

**You are charged only for results that actually came back.** No mock rows, no placeholder padding, no minimum charge on a run that found nothing. If an account is suspended, protected, or does not exist, the run reports why and charges you nothing for it.

| | |
|---|---|
| **Input** | X / Twitter handles, profile URLs, or numeric user IDs |
| **Output** | Tweets (text, metrics, media, mentions, links) + enriched author profiles |
| **Price** | $0.005 per account + $0.0003 per tweet ($0.30 per 1,000) |
| **Login required** | No — and that is why it does not break when scraper accounts get banned |
| **Keyword search** | **Not supported.** See [What this Actor cannot do](#what-this-actor-cannot-do) |

***

### What you get

#### Tweets

Every tweet record carries the text (including full long-form posts, which many scrapers truncate at 280 characters), timestamps, all six engagement counters, and fully expanded entities:

```json
{
  "itemType": "tweet",
  "type": "tweet",
  "id": "2090487779268407625",
  "url": "https://x.com/NASA/status/2090487779268407625",
  "text": "Our Artemis II crew is one step closer to the Moon.",
  "isLongform": false,
  "lang": "en",
  "createdAt": "2026-08-19T14:02:11.000Z",
  "source": "Twitter Media Studio",
  "replyCount": 412, "retweetCount": 3105, "quoteCount": 188,
  "likeCount": 24907, "bookmarkCount": 601, "viewCount": 1840233,
  "conversationId": "2090487779268407625",
  "isRetweet": false, "isQuote": false, "isReply": false,
  "hashtags": ["Artemis"],
  "cashtags": [],
  "mentions": [{ "userName": "NASAArtemis", "userId": "1050393...", "name": "NASA Artemis" }],
  "urls": ["https://www.nasa.gov/artemis-ii/"],
  "media": [{ "type": "photo", "url": "https://pbs.twimg.com/media/...jpg", "altText": "Four astronauts in orange flight suits" }],
  "authorUserName": "NASA",
  "author": { "...full profile, see below..." }
}
```

Retweets and quote tweets keep their lineage — `retweetedTweetId`, `retweetedAuthor`, `quotedTweetId`, `quotedText` — so you can tell original content from amplification without a second pass.

#### Profiles

The profile record goes past the usual follower-count-and-bio. These fields come straight from X's own account metadata; none of them are guessed, inferred, or generated by a language model:

| Field | Why it matters |
|---|---|
| `verifiedType` | `Government`, `Business`, or blank — separates official accounts from paid blue checks |
| `verifiedSince` | Exact ISO timestamp the account was verified |
| `parodyCommentaryFanLabel` | X's mandatory label for parody / commentary / fan accounts — a direct impersonation signal |
| `affiliatesCount` | How many sub-accounts a business account has under it |
| `professionalCategory` | Self-declared business category for professional accounts |
| `tipjar` | Bitcoin, Ethereum, PayPal, Venmo, Cash App, Patreon, GoFundMe and Bandcamp handles the account has published |
| `birthdate` | Publicly published birthdate, with its visibility setting |
| `isProtected`, `hasGraduatedAccess`, `possiblySensitive` | Account state flags |

Plus the standard set: `userId`, `name`, `description`, `descriptionUrls` (expanded, not `t.co`), `website`, `location`, `createdAt`, `followers`, `following`, `tweetsCount`, `mediaCount`, `favouritesCount`, `isBlueVerified`, `profilePicture` (full resolution), `coverPicture`.

***

### Input

```json
{
  "twitterHandles": ["nasa", "openai"],
  "startUrls": [{ "url": "https://x.com/elonmusk" }],
  "maxTweetsPerProfile": 200,
  "includeRetweets": false,
  "since": "2026-01-01"
}
```

| Option | Default | Notes |
|---|---|---|
| `twitterHandles` | — | With or without `@` |
| `startUrls` | — | Any `x.com/<handle>` or `twitter.com/<handle>` URL |
| `userIds` | — | Numeric IDs, immune to username changes |
| `maxTweetsPerProfile` | `100` | `0` fetches the profile only |
| `maxItems` | `0` | Hard cap across the whole run — bounds your spend |
| `scrapeProfilesOnly` | `false` | Cheapest mode for bulk account enrichment |
| `includeReplies` | `true` | |
| `includeRetweets` | `true` | |
| `onlyWithMedia` | `false` | |
| `since` / `until` | — | ISO dates; `since` also stops pagination early, so it makes runs cheaper |
| `includeAuthorProfile` | `true` | Turn off for a slimmer dataset |

***

### Pricing

Pay-per-event. You pay for what came back, nothing else.

| Event | Price | What it covers |
|---|---|---|
| `profile-scraped` | **$0.005** per account | Looking the account up and returning its enriched profile. Charged once per account. |
| `tweet-scraped` | **$0.0003** per tweet | Each tweet returned from that account's timeline — $0.30 per 1,000. |

Two events instead of one flat per-row price, because the work is shaped that way: resolving an
account costs the same whether you then pull 10 tweets from it or 2,000. Splitting it out is what
lets the per-tweet rate stay at $0.30/1K instead of being marked up to subsidise account lookups.

**What it works out to**, against the closest comparable Actor on the Store, which charges
$0.016 per profile plus $0.0004 per tweet:

| You want | Here | Closest comparable |
|---|---|---|
| 1 account, 10 tweets *(monitoring)* | **$0.008** | $0.016 |
| 1 account, 100 tweets | **$0.035** | $0.040 |
| 1 account, 2,000 tweets *(archive)* | **$0.605** | $0.800 |
| 10 accounts, 200 tweets each | **$0.650** | $0.920 |

Cheaper than that one at every volume, not just at the volume that makes the table look good.

Being straight about the rest of the field: a few Actors run a flat per-tweet rate slightly below
this one and come out ahead on very large single-account pulls. They are also doing keyword search,
which means they are driving a pool of logged-in accounts. That is a different tool with a different
failure mode — see [What this Actor cannot do](#what-this-actor-cannot-do).

Three commitments:

1. **Real results only.** Every charged row is data that came back from X. Padding a thin result set with placeholder rows and billing for them is a thing that happens in this category; it does not happen here.
2. **Failures are free.** Suspended, deleted, protected, or misspelled accounts produce an explanatory row and cost nothing — including the account lookup fee.
3. **No plan discrimination.** Free-plan runs get the same code path, the same data, and the same reliability as everyone else.

> There is a $0.01 floor on the run budget you can set. That is a floor on the *budget cap*, not a
> minimum charge — a run that returns one profile still costs you $0.005.

#### What you are not charged for

Those three commitments are not abstract. Each one names a specific thing that this Actor does
not do, and every item below is checkable against a run log:

| | What happens here |
|---|---|
| **Empty or placeholder rows** | A tweet that arrives with an ID but no text, no timestamp, and no metrics is not a result. It is dropped before it reaches the dataset, so it is never billed. |
| **Runs that find nothing** | A suspended, deleted, protected, or misspelled account produces one explanatory row marked `chargedForThis: false`. The account fee is not charged either. |
| **The free plan** | Same code path, same fields, same page depth. No demo mode, no 10-row cap, no nulls in place of data. |
| **Plan-tiered pricing** | $0.005 per account and $0.0003 per tweet on **every** Apify plan — Free, Bronze, Silver, Gold, Platinum, Diamond. Check the Pricing tab: all six columns are identical. |

That last row is worth checking on any Actor you are comparing, including this one. The Pricing
tab shows the per-plan breakdown, and the number a listing advertises in its title is not always
the number a free-plan run pays.

#### Many accounts, few tweets each

If you are monitoring a set of accounts rather than archiving one, you want a cap that applies
**per account**, in a single run. That is the default here:

```json
{
  "twitterHandles": ["NASA", "NASAHubble", "NASAWebb", "NASAPersevere", "NASAMars"],
  "maxTweetsPerProfile": 3,
  "since": "2026-06-01"
}
```

One run, five accounts, at most three tweets each, nothing older than the cutoff. Real output:

```
NASA            profile + 3 tweets
NASAHubble      profile + 3 tweets
NASAWebb        profile + 3 tweets
NASAMars        profile + 3 tweets
NASAPersevere   profile + 0 tweets   ← nothing past the cutoff, so nothing billed
                                       charged: 12 tweets, 5 accounts = $0.0286
```

`since` / `until` filter on the tweet timestamp and stop paging once the timeline passes the
cutoff, so a narrow window costs less than a wide one rather than the same.

***

### What this Actor cannot do

Being straight about this up front so you do not buy the wrong tool:

- **No keyword or hashtag search.** X does not expose search to logged-out clients at all — the endpoint is rejected at the edge before it reaches a backend. Any scraper that offers keyword search is driving it through a pool of logged-in accounts. This Actor deliberately does not do that, which is the same reason it cannot search. If you need search, you need a different tool.
- **No followers or following lists.** Same restriction.
- **No likes, bookmarks, or reply threads** of an arbitrary tweet.
- **Protected accounts** return their profile only. Their tweets are not public, and no scraper can change that.

What it does do — profiles and timelines by account — it does without a single login, which is why it is not exposed to the account-ban failures that take login-based scrapers offline.

***

### How it stays working

X rotates the internal query identifiers its own web app uses. Scrapers that hardcode them break silently, usually returning empty results rather than an error.

This Actor reads those identifiers from X's live frontend at runtime and caches them for six hours. If a call comes back rejected, it re-reads them and retries once. That repair path is plain pattern matching against the frontend bundle — deterministic, auditable, and with no language model anywhere in the data path, so your output cannot drift between runs.

***

### FAQ

**Do I need a Twitter/X API key?**
No. There is no key, no developer account, and no OAuth app to register.

**Do I need to supply cookies or a logged-in session?**
No. Actors that ask for an `auth_token` cookie are running on someone's logged-in account, which puts that account at risk of suspension and takes the scraper down with it.

**Do I need proxies?**
No. Proxy configuration is not an input because the Actor does not need one.

**How many tweets can I get from one account?**
The timeline pages back roughly 15–20 tweets at a time and keeps going. Set `maxTweetsPerProfile` to whatever you need; use `since` when you only care about a recent window.

**Can I get tweets older than the timeline goes back?**
X's own timeline is the ceiling. For deep historical archives you want a data provider, not a scraper.

**What happens if X changes something?**
Query identifier rotation is handled automatically (see above). Structural changes are not, and would need an update — if output looks wrong, open an issue on the Actor and it gets fixed.

**Is scraping X legal?**
Scraping publicly accessible pages has been treated differently from accessing a computer without authorization — see *hiQ Labs v. LinkedIn*, where the Ninth Circuit held that scraping public profiles did not violate the Computer Fraud and Abuse Act. That said, this is general background and not legal advice: platform terms, your jurisdiction, and what you do with the data all matter, and personal data carries its own obligations under GDPR and similar regimes. Get advice for your own situation.

***

### Integrating

Every Apify Actor exposes a REST API, so you can start a run and read results from any language:

```bash
curl -X POST "https://api.apify.com/v2/acts/<ACTOR_ID>/run-sync-get-dataset-items?token=<TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"twitterHandles":["nasa"],"maxTweetsPerProfile":50}'
```

Official clients exist for JavaScript and Python, and results can be exported as JSON, CSV, or Excel, or pushed onward through Apify integrations.

# Actor input Schema

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

Usernames to scrape, with or without the @ (e.g. "nasa", "@openai"). This is the fastest way to target accounts. Defaults to a single well-known account so a run with no input still returns data.

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

Profile URLs such as https://x.com/nasa. Handy when your source system already stores links rather than handles.

## `userIds` (type: `array`):

Numeric X user IDs (e.g. "11348282"). Use these when you need results that survive a username change.

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

How many tweets to pull from each account's timeline. Set to 0 to fetch profiles only. You are charged only for tweets actually returned.

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

Hard cap across the whole run, counting every profile and tweet pushed. 0 means no cap. Use it to bound spend on large input lists.

## `scrapeProfilesOnly` (type: `boolean`):

Return one enriched profile record per account and fetch no timelines at all. The cheapest mode for bulk account enrichment.

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

Keep the account's replies. Turn off to get only standalone posts.

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

Keep reposts of other accounts. Turn off to get only the account's own content.

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

Keep only tweets carrying a photo, video, or GIF.

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

ISO date or datetime, e.g. 2026-01-01. Timelines are walked newest-first and stop once they pass this date, so a tight range also makes the run cheaper.

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

ISO date or datetime, e.g. 2026-06-30. Tweets newer than this are skipped.

## `includeAuthorProfile` (type: `boolean`):

Attach the full author record to every tweet. Turn off for a slimmer dataset when you already store profiles separately.

## Actor input object example

```json
{
  "twitterHandles": [
    "nasa",
    "openai"
  ],
  "maxTweetsPerProfile": 100,
  "maxItems": 0,
  "scrapeProfilesOnly": false,
  "includeReplies": true,
  "includeRetweets": true,
  "onlyWithMedia": false,
  "includeAuthorProfile": true
}
```

# Actor output Schema

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

One row per tweet, with full text (long-form posts are not truncated), engagement counters, media, mentions, hashtags and expanded links.

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

One row per account, including deterministic fields most scrapers omit: verification type and date, parody/commentary/fan label, affiliate count, published tipjar handles.

## `allItems` (type: `string`):

The complete dataset in one call. Each row carries an itemType field of either 'tweet' or 'profile'.

## `runSummary` (type: `string`):

What was scraped, what could not be scraped and why, and exactly how many billable events were charged.

# 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 = {
    "twitterHandles": [
        "nasa",
        "openai"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nick.cheng/x-twitter-profile-tweets-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 = { "twitterHandles": [
        "nasa",
        "openai",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("nick.cheng/x-twitter-profile-tweets-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 '{
  "twitterHandles": [
    "nasa",
    "openai"
  ]
}' |
apify call nick.cheng/x-twitter-profile-tweets-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nick.cheng/x-twitter-profile-tweets-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/3dJpgbL5FEG0QYkSM/builds/GkJVL1RAdrpUxJ19I/openapi.json
