# Tweet Scraper by URL or ID (`renzomacar/tweet-scraper-by-url-or-id`) Actor

Turn any tweet URL or ID into a full JSON record: text, likes, retweets, quotes, bookmarks, replies, media, author. No login, no cookies. Same price on every Apify plan and empty results are never charged.

- **URL**: https://apify.com/renzomacar/tweet-scraper-by-url-or-id.md
- **Developed by:** [Renzo Madueno](https://apify.com/renzomacar) (community)
- **Categories:** Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.20 / 1,000 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

## Tweet Scraper by URL or ID

Give it a tweet URL or a tweet ID. Get back the whole tweet as clean JSON: text, timestamp, language, likes, retweets, replies, quotes, bookmarks, media, links, hashtags, mentions, and the author's public profile numbers.

One tweet or a hundred thousand. No login, no cookies, no browser, no account farm.

### The two things that make this different

**The price is the same on every Apify plan.** $0.20 per 1,000 tweets on the free plan, on Starter, on Scale, on Business. There is no free-tier surcharge, no reduced result cap for free users, and no version of this actor that costs 100x more because of which plan you happen to be on. This sounds like it should go without saying. It does not: the largest Twitter scraper on this Store charges $0.40/1k on paid plans and $40/1k on the free plan, and roughly half of its one-star reviews are people who found that out from an invoice.

**You are never charged for an empty result.** If a tweet is deleted, if the account was suspended, if the ID is malformed, if your date filter excludes it — no row is written and no charge is made. There is no `{"noResults": true}` object padding your dataset. The run log tells you exactly how many results were suppressed and why.

Those two sentences are the entire product thesis. Everything below is detail.

### Input

Paste tweets in whatever form you already have them:

```json
{
  "urls": [
    "https://x.com/jack/status/20",
    "https://twitter.com/elonmusk/status/1349129669258448897",
    "1519480761749016577"
  ]
}
```

Full `x.com` URLs, legacy `twitter.com` URLs, `/statuses/` URLs, URLs with tracking parameters, and bare numeric IDs all work. So does mixing them in one list.

The field is called `urls`, but `url`, `tweetUrls`, `tweetUrl`, `ids`, `id`, `tweetIds`, `tweetId` and `startUrls` are all accepted as aliases. You should not have to read documentation to find out what a field is called this week.

| Field | Type | What it does |
|---|---|---|
| `urls` | array | Tweet URLs or IDs. Aliases: `url`, `tweetUrls`, `ids`, `tweetIds`, `startUrls`. |
| `maxItems` | integer | Hard ceiling on delivered rows. Aliases: `maxResults`, `limit`, `maxTweets`. |
| `postedAfter` | string | Only tweets posted on or after this. Alias: `since`. |
| `postedBefore` | string | Only tweets posted on or before this. Alias: `until`. |
| `preferSyndication` | boolean | Advanced. Try the token-free endpoint first. Off by default. |

Dates accept `YYYY-MM-DD`, a full ISO timestamp, or a relative window like `7 days`, `3 weeks`, `6 months`. An unparseable date raises an error naming the field and showing what you passed, rather than being silently dropped.

#### A single tweet is allowed

You can run this on exactly one tweet. There is no minimum batch, no clause anywhere telling you that retrieving a single tweet is "strictly forbidden", and nothing in the code that treats a one-item run as abuse. A one-tweet run costs $0.0002.

### Output

One row per tweet. Every field below was observed in live responses, and the percentage is the measured fill rate across a verification sample run on 2026-08-22.

| Field | Fill | Notes |
|---|---|---|
| `id`, `url`, `text`, `createdAt`, `lang` | 100% | `createdAt` is ISO 8601 UTC. |
| `likeCount` | 100% | |
| `retweetCount`, `quoteCount`, `bookmarkCount` | 100% | From the primary endpoint. See the honesty note below. |
| `replyCount` | 100% | |
| `conversationId` | 100% | The thread root, useful for grouping. |
| `isReply`, `isQuote`, `isRetweet`, `isLongform` | 100% | Booleans, always present. |
| `authorHandle`, `authorName`, `authorId`, `authorUrl` | 100% | |
| `authorFollowers`, `authorIsBlueVerified` | 100% | |
| `authorProfileImageUrl` | 100% | |
| `source` | 100% | "Twitter for iPhone", "TweetDeck Web App", etc. |
| `media` | ~40% | Array of `{type, url, width, height, expandedUrl}`. Empty when the tweet has no attachments. |
| `possiblySensitive` | ~40% | |
| `inReplyToTweetId`, `inReplyToHandle` | ~20% | Present only on replies, by definition. |
| `hashtags`, `mentions`, `links` | varies | Arrays. Empty when the tweet contains none. |
| `dataSource` | 100% | `graphql` or `syndication`. Tells you which surface answered. |
| `requestedId`, `scrapedAt` | 100% | Your input echoed back, and the fetch time. |

#### Long posts come back whole

X stores a long post twice: a truncated ~277-character stub in the legacy field, and the real text in a separate `note_tweet` field. A scraper that reads the obvious field hands you the stub with the rest silently missing.

This one reads `note_tweet` and flags the row with `isLongform: true`. Measured on 2026-08-22: a post by `@pmarca` whose stub is 277 characters is delivered here at its full **2,841 characters**; one by `@ylecun` at 2,434. If your dataset from another tool is full of posts that stop at 277 characters, this is why.

#### Data integrity, not just data presence

A field arriving is not the same as a field being right, so the counters are normalised before they reach you:

- Abbreviated display values (`"1.5M"`, `"428.9K"`) are converted to integers, so a column never mixes strings and numbers.
- **Negative counts are returned as `null`.** A 32-bit overflow on a very large account shows up as a negative like `-576135159`. That is not a measurement, and passing it through would silently poison any average computed over the column.
- Dates are parsed from X's own format (`Wed Oct 10 20:19:24 +0000 2018`) into ISO 8601. A value that cannot be read becomes `null`, never a 1970 epoch date that would quietly slip through your date filter.
- `t.co` shortlinks are resolved: `links` holds the real destination URLs and `media` holds the real media URLs. The `text` field is left exactly as X returns it, shortlinks included, so it still matches the tweet you see on screen.

#### What is deliberately absent

**View counts.** X does not send view counts to logged-out clients on any endpoint. The `views` object comes back as `{"state": "Enabled"}` with no number in it. This actor therefore does not have a `viewCount` field at all, rather than shipping one that is always null or, worse, one filled with an estimate. If you need real view counts, you need the official paid X API.

**Email addresses and phone numbers.** X does not publish them. Nobody scraping X can produce them honestly.

### How it works

Two public surfaces, tried in order:

1. **GraphQL `TweetResultByRestId`** with a guest token. This is the same call x.com makes for a visitor who is not signed in. A guest token comes from `POST /1.1/guest/activate.json`, costs nothing, requires no account, and is refreshed automatically inside the run whenever X rotates or rejects it. This path carries the full metric set.

2. **`cdn.syndication.twimg.com/tweet-result`**, the endpoint behind embedded tweets. It needs no token whatsoever, which makes it the resilient fallback when the guest-token path is being rate limited. It publishes likes and reply counts but **not** retweet, quote or bookmark counts.

When a row comes from the fallback, `dataSource` is `"syndication"` and `retweetCount`, `quoteCount` and `bookmarkCount` are `null` — not `0`. A zero would look like a real measurement and would quietly corrupt any average you compute. A null says "not available from this surface", which is the truth. The run log reports how many rows came from which source.

The actor uses no browser, no proxy by default, and no Twitter account. Memory footprint is 512 MB, so compute cost is close to nothing next to the per-result price.

### Errors go where you can act on them

Failed inputs are never written into the dataset. They go into a key-value record called `FAILURES`, alongside a summary of the run:

```json
{
  "actor": "Tweet Scraper by URL or ID",
  "targetsRequested": 4,
  "resultsDelivered": 2,
  "resultsCharged": 2,
  "emptyRowsSuppressed": 0,
  "failureCount": 2,
  "failures": [
    { "target": "total nonsense", "error": "Not a tweet URL or numeric tweet ID" },
    { "target": "1613611384712065024", "error": "Tweet is deleted, private, or from a suspended account - X returns nothing for it" }
  ],
  "notes": ["1 tweet(s) no longer exist publicly. They were not charged."]
}
```

Your dataset stays clean enough to load straight into a database without filtering out sentinel objects first.

**A run that delivers nothing is marked FAILED.** If every tweet you asked for is deleted, or your date window excludes all of them, the run does not finish green over an empty dataset. It fails, with a message naming the cause. You are still charged nothing.

### Date filters actually filter

`postedAfter` and `postedBefore` are applied to every row before it is written and before it is billed. A tweet outside the window is not stored and not charged, and the count of excluded tweets appears in the run notes.

If you pass a window that cannot contain anything — `postedAfter` later than `postedBefore` — the run stops immediately with an explicit error rather than working through your whole input list to produce nothing.

### Pricing

**$0.20 per 1,000 tweets. Pay per result. No monthly subscription. No platform usage fee on top. Identical on every Apify plan.**

| Tweets | Cost |
|---|---|
| 1 | $0.0002 |
| 1,000 | $0.20 |
| 10,000 | $2.00 |
| 100,000 | $20.00 |

Deleted tweets, suspended accounts, malformed inputs and date-filtered rows all cost $0.00.

### Typical uses

- **Reconstruct a link dump.** You have 40,000 tweet URLs collected from newsletters, Slack, a research corpus or someone else's dataset, and you need the actual content and engagement behind them.
- **Track a specific set of posts over time.** Re-run the same ID list daily and diff the metrics to build an engagement curve.
- **Rehydrate an academic dataset.** Research corpora are distributed as bare tweet IDs for compliance reasons. This turns them back into tweets, and tells you honestly which ones have since been deleted rather than silently dropping them.
- **Verify a screenshot.** One URL in, the real numbers out.
- **Feed a pipeline.** Chain it after any actor that produces tweet links.

### Limits worth knowing before you start

- **No keyword search.** This actor takes URLs and IDs. It does not search X by keyword or hashtag, because X blocks the search timeline for guests entirely, and the scrapers that offer it anyway are running pools of real accounts to get around that. That is the machinery behind most of the reliability complaints in this category. This actor does not have it and will not grow it.
- **Protected accounts return nothing.** X publishes nothing about them to logged-out clients. They land in `FAILURES`.
- **Very large parallel runs** may see the guest-token path rate limited. The actor renews tokens automatically and falls back to the token-free surface, but if you are pushing hundreds of thousands of tweets, set `preferSyndication` and accept null retweet/quote/bookmark counts, or spread the work across runs.

### Verified

Every endpoint, field and fill rate in this README was tested live against X on **2026-08-22** from a datacenter IP with no cookies and no account. Nothing here is copied from documentation that may have gone stale.

### Related actors

- **X Profile Posts Scraper - No Cookies** — a whole account's timeline, with a real per-account limit.
- **X Profile Scraper - No Login** — bulk handle-to-profile lookup.
- **Tweet Replies Scraper - No Login** — the replies X shows publicly on a tweet.
- **X Trends Scraper by Country** — trending topics for 62 countries and 400+ cities.

Same pricing philosophy across all of them: one price on every plan, and nothing charged for nothing.

# Actor input Schema

## `urls` (type: `array`):

One tweet per line. Accepts full URLs (https://x.com/jack/status/20), twitter.com URLs, or bare numeric IDs (20). A single tweet is perfectly fine - there is no minimum and no rule against it here.

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

Hard stop on delivered rows. Leave empty for no cap. You are only charged for rows actually delivered, so this is a budget ceiling, not a suggestion.

## `postedAfter` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as "7 days". Tweets outside the window are dropped before billing, so you are not charged for them.

## `postedBefore` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as "1 day". Applied together with "Only tweets posted after".

## `preferSyndication` (type: `boolean`):

Off by default. When on, the token-free syndication endpoint is tried first. It is more resilient under heavy parallel load but does not publish retweet, quote or bookmark counts - those fields come back null rather than as a fake zero.

## Actor input object example

```json
{
  "urls": [
    "https://x.com/jack/status/20"
  ],
  "postedAfter": "2024-01-01",
  "postedBefore": "2025-12-31",
  "preferSyndication": false
}
```

# Actor output Schema

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

One row per tweet: full text, engagement counts, media and author.

# 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 = {
    "urls": [
        "https://x.com/jack/status/20",
        "1349129669258448897"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/tweet-scraper-by-url-or-id").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 = { "urls": [
        "https://x.com/jack/status/20",
        "1349129669258448897",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/tweet-scraper-by-url-or-id").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 '{
  "urls": [
    "https://x.com/jack/status/20",
    "1349129669258448897"
  ]
}' |
apify call renzomacar/tweet-scraper-by-url-or-id --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,renzomacar/tweet-scraper-by-url-or-id"
        }
    }
}

```

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/cKeKgfOVyenN6SonQ/builds/BdFalVD2EYMFRBqqP/openapi.json
