# X (Twitter) Latest Posts Monitor (`simple.actors/x-profile-posts`) Actor

Check any public X (Twitter) profile for its latest posts: full text, links, engagement counts, media and author. No login, no cookies, no API key — nothing to get suspended or rate-limited. Built for polling many accounts often rather than deep history: 5 latest posts per profile. Unofficial.

- **URL**: https://apify.com/simple.actors/x-profile-posts.md
- **Developed by:** [Simple Actors](https://apify.com/simple.actors) (community)
- **Categories:** Social media, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 1,000 profile reads

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) Latest Posts Monitor

Check any public X (Twitter) profile for what it has posted lately — full text,
permanent link, publish date, likes, reposts, views, photos and video, newest
first. 49 fields per post plus a 33-field author record.

**No login, no cookies, no API key, no account of yours involved.** It reads what
X already renders for logged-out visitors, so there is no session to keep alive,
no account to get suspended, and nothing to re-authenticate when it breaks —
because there is nothing to break. A run is **one request per profile** and
finishes in about five seconds.

**$0.50 per 1,000 profiles read** — Apify platform usage included, nothing else
to pay. The charge is per profile and does not depend on how many posts come
back, so a full read and a check that finds one new post cost the same. A profile
that cannot be read — suspended, protected, or no such account — is never
charged.

> **Read this before you start: 5 posts per profile.** That is X's limit for a
> logged-out reader, not a setting, and no option raises it. This is built to
> check *many* accounts *often* — if you need one account's deep history, this is
> the wrong tool and you will be disappointed.

*Unofficial. Not affiliated with, endorsed by, or sponsored by X Corp.*

### What you can do with it

- **Watch accounts for new posts** — run it on a schedule across as many
  profiles as you like and diff on `id`. This is what it is for.
- **Follow announcements** — agencies, transit operators, emergency services,
  sports teams and newsrooms post breaking updates to X first.
- **Track engagement** — likes, replies, reposts, quotes, bookmarks and views on
  every post, so you can see which ones travelled.
- **Collect media** — photos and videos with direct URLs, dimensions and
  alt text.
- **Feed a dashboard or an alert** — full post text and links, ready to route.

### How to use it

Paste one or more accounts:

```json
{
  "startUrls": [{ "url": "https://x.com/NASA" }]
}
```

A profile URL, a `twitter.com` URL or a link to one of their posts all work —
the account is taken from whichever you give.

If what you have is handles rather than links, put them in `handles` instead.
Both lists are read as one, so a mixed source is fine:

```json
{
  "startUrls": [
    { "url": "https://x.com/NASA" },
    { "url": "https://twitter.com/WHO" }
  ],
  "handles": ["espn", "@natgeo"]
}
```

They are two inputs rather than one because Apify checks the URL list is really
URLs before the run starts, so a bare handle cannot travel in it.

### What you get

One dataset item per post, newest first. A shortened example:

```json
{
  "type": "tweet",
  "id": "2090877991228264814",
  "url": "https://x.com/NASA/status/2090877991228264814",
  "text": "Launching soon, our newest space telescope @NASARoman is equipped with tools to help it spot and study exoplanets orbiting distant stars.\n\nLearn about these tools and how they work in our newest NASA's Curious Universe podcast on Roman: https://t.co/nCZCpF1MIO",
  "likeCount": 878,
  "replyCount": 65,
  "retweetCount": 138,
  "quoteCount": 6,
  "bookmarkCount": 39,
  "viewCount": 411170,
  "createdAt": "Fri Aug 21 19:05:34 +0000 2026",
  "createdAtIso": "2026-08-21T19:05:34.000Z",
  "timestamp": 1787339134000,
  "isReply": false,
  "isQuote": false,
  "isPinned": false,
  "isLongForm": false,
  "mentions": ["NASARoman"],
  "urls": ["https://go.nasa.gov/45FxOK8"],
  "media": [
    {
      "type": "video",
      "mediaUrl": "https://pbs.twimg.com/media/HQRKc2lWgAAtNGa.png",
      "videoUrl": "https://video.twimg.com/amplify_video/.../7INz5crAEofwRbWp.mp4",
      "durationMillis": 28361,
      "width": 720,
      "height": 406
    }
  ],
  "quote": null,
  "author": {
    "userName": "NASA",
    "name": "NASA",
    "id": "11348282",
    "followers": 92346035,
    "following": 118,
    "isBlueVerified": true,
    "verifiedType": "Government",
    "description": "Making the seemingly impossible, possible. ✨",
    "location": "Pale Blue Dot",
    "profilePicture": "https://pbs.twimg.com/profile_images/...jpg"
  },
  "profileUrl": "https://x.com/NASA",
  "scrapedAt": "2026-08-22T04:17:01.155Z"
}
```

The field names are the ones already used by widely-used X post datasets, so
code written against those reads this output unchanged.

#### Dates

Every post carries the same instant three ways, because different tools want
different things:

| Field | Example | For |
|---|---|---|
| `createdAt` | `Fri Aug 21 19:05:34 +0000 2026` | X's own format, for existing parsers |
| `createdAtIso` | `2026-08-21T19:05:34.000Z` | Sorting, filtering, spreadsheets |
| `timestamp` | `1787339134000` | Arithmetic, milliseconds since the epoch |

#### Long posts are returned in full

A post past the classic length limit is stored by X twice: a visible copy cut
off mid-sentence, and the whole thing separately. This returns the whole thing
and sets `isLongForm` so you know it happened.

#### Quote posts

When a post quotes another, `quote` carries the quoted post in the same shape —
its own text, author and engagement counts. The quoting post's own counts stay
on the top level, so the two are never mixed up. Quoting stops one level deep.

#### One count X does not give out

If the author restricted who may reply, X does not disclose that post's reply
count to a logged-out reader — it reports zero. Passing that on would be wrong,
so `replyCount` is **null** on those posts and `whoCanReply` says why
(`"Community"` or `"ByInvitation"`). Every other count is real.

This is worth knowing if you compare a result against the site while logged in:
X shows you the reply count there because you are signed in, and this cannot see
it. Likes, reposts, quotes, bookmarks and views are unaffected.

#### Also on every post

`whoCanReply` when the author narrowed replies, `isEdited` with the full
`editHistoryIds` (every version the post has had, oldest first), `communityNote`
when one is attached, `socialContext` for why X surfaced the post,
`inReplyToId`, `displayTextRange`, and `isArticle`.

The author record includes both label systems X uses — `affiliateLabel` for the
organisation an account belongs to, and `identityLabel` with its badge artwork
and link for business and government accounts — plus a full-size avatar URL
alongside the thumbnail.

#### Pinned posts

A pinned post is included and flagged `isPinned`. X returns it first regardless
of its age, and it can be years older than the rest — so items are sorted by
publication date, not by the order X sends them.

#### Link preview cards

When a post links somewhere, `card` carries the preview X built for it: `title`,
`description`, `domain`, and a preview `image` with dimensions. Everything X
supplied is kept under `card.bindings`, so card types this does not name — live
broadcasts, polls — are still readable.

#### Media

`media` holds photos and videos with dimensions, duration, alt text and any
title or description. `videoUrl` is the best-quality MP4, `videoVariants` lists
every rendition with its bitrate if you want a smaller file, and `streamUrl` is
the HLS playlist for adaptive playback in a player.

#### Links, mentions and hashtags

The flat `urls`, `mentions`, `hashtags` and `cashtags` arrays cover the common
case. `entities` carries the same things in full: for each link, the `t.co` as it
appears in the post text, the address it expands to, and the short form X
displays — plus the character positions, so you can substitute links back into
the text. Mentions carry the account ID as well as the handle.

On a long post these come from the same copy of the text you get in `text`, so
the positions always line up and nothing in the tail is missed.

**Media URLs are signed by X and expire within hours.** Download them promptly;
do not store them as long-term links. The post's `url` is the stable one.

### What an empty result means

An empty dataset means the account was read and has posted nothing in the window
you asked for. It never means "we could not look".

An account that cannot be read is reported as its **own row** carrying `error`
and `errorDescription`, so one bad account in a batch never costs you the rest:

| `error` | Meaning |
|---|---|
| `not_found` | No such account |
| `not_available` | Suspended, withheld in this region, or otherwise restricted |
| `protected` | Posts are visible only to approved followers, so a login would be needed |
| `window_too_wide` | `onlyPostsNewerThan` reaches further back than this run can see — see below |
| `read_failed` | X did not serve a readable page after several attempts |

If *every* account in a run fails, the run itself fails, so a scheduled job
cannot mistake a bad run for a quiet day.

### What it costs

**$0.50 per 1,000 profiles read.** One profile read is one charge, so asking for
five profiles in one run costs five. There is no per-post fee and no per-run fee.

Because the charge is per profile, what you pay per *post* depends on how many
posts a read returns:

| Posts returned by a read | Works out at |
|---|---|
| 5 — a full read | $0.10 per 1,000 posts |
| 3 | $0.17 per 1,000 posts |
| 1 — a poll that found one new post | $0.50 per 1,000 posts |

So the price rewards full reads and costs the same on a quiet one. If you are
polling with `onlyPostsNewerThan` and most checks come back empty, budget by
profiles checked rather than by posts collected — that is the number you are
actually billed on.

Two more consequences worth knowing:

- **Take all five posts.** Fewer posts cost you no less, because the price is for
  reading the profile, not for what comes back.
- **Batch your profiles.** Sending twenty profiles in one run costs exactly the
  same as twenty separate runs, but finishes far faster and in a single call.

A profile that cannot be read is never charged, so a list containing a few dead
accounts costs only for the live ones.

### Settings

| Setting | Default | What it does |
|---|---|---|
| `startUrls` | — | Accounts to read, as links: profile URL, `twitter.com` URL, or a link to one of their posts. |
| `handles` | — | Accounts to read, as handles: `NASA`, `@NASA` or `nasa`. Combined with `startUrls`. |
| `maxPosts` | `5` | How many of the latest posts per account. 5 is the ceiling — see Limits. |
| `onlyPostsNewerThan` | — | Keep only posts after a point: `"20 hours"`, `"last 3 days"`, or `2026-08-01`. |
| `includeRaw` | `false` | Attach X's untouched post record under `raw`. Makes items much larger. |
| `proxy` | Datacenter | Apify Proxy settings. Switch to residential only if accounts start coming back unreadable. |

#### `onlyPostsNewerThan` will refuse rather than mislead

A run sees an account's latest posts and no further back. If your window reaches
past them, some posts inside it were never fetched — and returning what was
found would read as "this is everything since then".

So the run **fails** in that case instead, telling you how far back it could
actually see. That is what lets an empty result mean "nothing new" and nothing
else. A pinned post does not count towards that reach, since it can be years old
and would otherwise make any window look covered.

If you hit this, use a shorter window or run more often.

### Limits

**Five posts per account, per run.** This is X's limit, not a setting we chose.
X renders five posts to a logged-out reader, then ends the timeline and withholds
the cursor that would ask for the next page. `maxPosts` is capped at 5 and values
above it are rejected at input validation rather than silently under-delivered.

If you need an account's deep history, this is the wrong tool — it is built for
freshness, not depth. For monitoring, five posts per run is normally plenty:
run it more often rather than asking for more.

**Original posts only.** The logged-out profile timeline carries an account's own
posts, including its replies and quote posts, but not its reposts of others.

**No search, and no other tabs.** X serves nothing to a logged-out reader for
search results, the Media tab or the Highlights tab, so this reads profiles only.

**Protected accounts cannot be read.** They need an approved follower's login,
which this deliberately does not have.

**`lang` and `source` are always null.** X does not include them in what it
renders to logged-out readers. The fields are present so existing code does not
break on their absence.

### Call it from the API

```bash
curl -s "https://api.apify.com/v2/acts/simple.actors~x-profile-posts/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"startUrls":[{"url":"https://x.com/NASA"}]}'
```

### Note

This reads publicly visible posts only — the same ones anyone can see without
logging in. It does not log in, does not use cookies, and cannot reach protected
accounts, direct messages, or anything else behind a login.

# Actor input Schema

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

Public X accounts to read, as links: a profile URL (https://x.com/NASA), a twitter.com URL, or a link to one of their posts — the account is taken from it. For bare handles use "X handles" below, which the platform will accept as plain text. Protected accounts cannot be read without a login and are reported as such.

## `handles` (type: `array`):

Accounts given as handles rather than URLs — "NASA", "@NASA" or "nasa" all work. Use this when you have a column of handles; use "X profiles" above when you have links. Both may be given together and are read as one list.

## `maxPosts` (type: `integer`):

How many of the latest posts to return per account, newest first. Defaults to 5, which is what X renders for a logged-out reader. Asking for more is not possible: X ends the timeline after those posts and withholds the cursor that would fetch the next page, so 5 is the ceiling. Lower it if you only want the newest few.

## `proxy` (type: `object`):

Defaults to Apify's datacenter proxy, which is included in every plan and is enough for X in normal use. Switch to residential only if you see accounts coming back as unreadable — that is what a challenged datacenter address looks like.

## `includeRaw` (type: `boolean`):

Attach X's untouched post record under `raw`. Turn this on when you need a detail that has no named field; it makes each item much larger, so leave it off otherwise.

## `onlyPostsNewerThan` (type: `string`):

Keep only posts published after this point. Takes a window like "20 hours", "last 3 days" or "90 minutes", or an ISO date such as 2026-08-01. Because a run reads only an account's latest posts, the run fails rather than returning a partial answer if the window reaches further back than those posts do — so an empty result always means "nothing new", never "we did not look".

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://x.com/NASA"
    }
  ],
  "handles": [],
  "maxPosts": 5,
  "proxy": {
    "useApifyProxy": true
  },
  "includeRaw": false,
  "onlyPostsNewerThan": "20 hours"
}
```

# Actor output Schema

## `posts` (type: `string`):

One row per post with its headline numbers.

## `engagement` (type: `string`):

Likes, replies, reposts, quotes, bookmarks and views per post.

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

Posts carrying photos or video, with media URLs.

## `full` (type: `string`):

Complete post text with its author and flags.

## `raw` (type: `string`):

Every field of every item, exactly as pushed.

# 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 = {
    "startUrls": [
        {
            "url": "https://x.com/NASA"
        }
    ],
    "handles": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("simple.actors/x-profile-posts").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 = {
    "startUrls": [{ "url": "https://x.com/NASA" }],
    "handles": [],
}

# Run the Actor and wait for it to finish
run = client.actor("simple.actors/x-profile-posts").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 '{
  "startUrls": [
    {
      "url": "https://x.com/NASA"
    }
  ],
  "handles": []
}' |
apify call simple.actors/x-profile-posts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,simple.actors/x-profile-posts"
        }
    }
}

```

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/dgChUOY1OCR1yBGe3/builds/TvRunTFpNgBdQEc3y/openapi.json
