# X (Twitter) Scraper – Profile Posts & Stats (`simple.actors/x-profile-posts`) Actor

Scrape any public X (Twitter) profile for its latest posts — full text, permanent link, publish date, likes, reposts, views, photos and video — plus a 33-field author record. No login, no cookies, no API key. Built for polling many accounts often: 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, Marketing
- **Stats:** 9 total users, 6 monthly users, 98.2% 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) Scraper – Profile Posts & Stats

Scrape 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.** Built for social
media monitoring, breaking-news alerting, engagement tracking and competitor
research across many accounts at once.

**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, and does not fail the run: it comes back as a row saying why.

> **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.
>
> **Pick your row shape up front.** The default is one row per *post*, with the
> account record repeated on each — right for collecting posts, wasteful for
> looking up accounts. Set [`outputFormat: "profile"`](#two-shapes-one-row-per-post-or-one-row-per-account)
> and each account is a single row instead — its profile, with the latest posts
> attached — at the same price.

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

### Features

- **Latest posts per public profile** — 5 per account, newest first, 49 fields
  each.
- **Full author record** — 33 fields including followers, following, post count,
  bio, location, verification and join date.
- **Every engagement metric X shows** — likes, replies, reposts, quotes,
  bookmarks and views.
- **Long posts in full** — no truncation at the old character limit.
- **Quote posts carry the quoted post** with its own author and engagement.
- **Media with detail** — photos and videos with direct URLs, dimensions and alt
  text.
- **Link preview cards** — title, description and image where a post has one.
- **Entities parsed** — links, mentions and hashtags as their own fields, with
  `t.co` links resolved to the real destination.
- **Pinned posts flagged**, so an old pin never passes for the newest post.
- **Two output shapes at the same price** — one row per post, or one row per
  account with its posts nested.
- **Handles or URLs** — `x.com`, `twitter.com`, a link to a single post, or a
  bare handle.
- **Incremental polling** — `onlyPostsNewerThan` refuses rather than misleads if
  the window reaches past what one read can see.
- **Runs stay green.** A suspended, protected or nonexistent account is a row.

### Input

```json
{
  "startUrls": [{ "url": "https://x.com/NASA" }],  // profile, twitter.com or post URLs
  "handles": ["espn", "@natgeo"],   // or bare handles — both lists are read as one
  "outputFormat": "posts",          // "posts" = row per post, "profile" = row per account
  "maxPosts": 5,                    // latest posts per profile (5 is X's ceiling)
  "onlyPostsNewerThan": "3 days",   // optional window: "20 hours", "3 days", "2026-08-01"
  "includeRaw": false,              // attach X's untouched post object under `raw`
  "proxy": { "useApifyProxy": true }
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | array | — | Accounts as links. A profile URL, a `twitter.com` URL or a link to one of their posts all work. |
| `handles` | array | — | Accounts as bare handles: `espn` or `@natgeo`. Read together with `startUrls`. |
| `outputFormat` | string | `posts` | `posts` gives one row per post; `profile` gives one row per account with its posts nested. Same price. |
| `maxPosts` | integer | `5` | Latest posts per account. 5 is X's ceiling for a logged-out reader. |
| `onlyPostsNewerThan` | string | — | Keep only posts after a window or ISO date. |
| `includeRaw` | boolean | `false` | Attach X's untouched post object under `raw`. Makes rows much larger. |
| `proxy` | object | Apify Proxy | Apify Proxy settings. |

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.

### Output

One dataset item per post, newest first — or one per account, if you would rather
have the profile stated once with its posts attached
([both shapes](#two-shapes-one-row-per-post-or-one-row-per-account)). A shortened
example of the default:

```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.

#### Two shapes: one row per post, or one row per account

The item above is the default: **one row per post**, with the account record on
each of them. It is the right shape for collecting posts.

If what you want is the **account** — its picture, bio, follower count, website —
set `outputFormat` to `profile`. Each account becomes a single row: the profile
at the top level, its latest posts in short form under `posts`.

```json
{
  "type": "user",
  "userName": "NASA",
  "name": "NASA",
  "url": "https://x.com/NASA",
  "id": "11348282",
  "description": "Making the seemingly impossible, possible.",
  "location": "Pale Blue Dot",
  "website": "https://www.nasa.gov/",
  "followers": 92345916,
  "following": 190,
  "statusesCount": 76211,
  "profilePicture": "https://pbs.twimg.com/profile_images/%E2%80%A6_normal.jpg",
  "profilePictureFull": "https://pbs.twimg.com/profile_images/%E2%80%A6.jpg",
  "coverPicture": "https://pbs.twimg.com/profile_banners/%E2%80%A6",
  "isVerified": false,
  "isBlueVerified": true,
  "verifiedType": "Government",
  "isProtected": false,
  "createdAtIso": "2007-12-19T20:20:32.000Z",
  "postsReturned": 5,
  "posts": [
    {
      "id": "2090877991228264814",
      "type": "tweet",
      "url": "https://x.com/NASA/status/2090877991228264814",
      "twitterUrl": "https://twitter.com/NASA/status/2090877991228264814",
      "text": "Launching soon, our newest space telescope…",
      "createdAtIso": "2026-08-21T19:05:34.000Z",
      "timestamp": 1787339134000,
      "likeCount": 878,
      "retweetCount": 138,
      "replyCount": 65,
      "quoteCount": 6,
      "bookmarkCount": 39,
      "viewCount": 411170,
      "isReply": false,
      "isRetweet": false,
      "isQuote": false,
      "isPinned": false,
      "isSensitive": false,
      "lang": "en",
      "hashtags": [],
      "mentions": ["NASARoman"],
      "urls": ["https://go.nasa.gov/45FxOK8"],
      "media": [{ "type": "video", "mediaUrl": "https://pbs.twimg.com/media/%E2%80%A6png", "videoUrl": "https://video.twimg.com/%E2%80%A6mp4" }],
      "imageCount": 0,
      "videoCount": 1,
      "authorUserName": "NASA"
    }
  ]
}
```

That is the **complete** post shape in this format — all 26 fields, not an
excerpt. `media` is among them, so the pictures and video files are here too and
you do not need the row-per-post format to get them. `authorUserName` is kept
because a timeline carries reposts and quotes written by other people. What the
short form drops is the rest of the post record: reply and quote threading
(`inReplyToId`, `quote`), edit history, community notes, link preview `card`,
`place`, `source`, `whoCanReply` and the full nested `author`. If you need any of
those, use the default format.

Same charge either way — it is one account read. What it saves is size: the
account record is stated once instead of on all five posts.

#### 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.

### How to use

#### From Apify Console

1. Open the Actor and click **Try for free** / **Start**.
2. Paste profile links into **X profiles**, or a column of bare handles into
   **X handles** — the two lists are read as one, so a mixed source is fine.
3. Decide the row shape in **What a row is**: one row per post for collecting
   posts, one row per account for looking accounts up. Same price either way.
4. To poll for what is new, set **Only posts newer than** to a window shorter
   than the gap between your runs, then schedule the run.
5. Click **Start**, then open the **Dataset** tab and export as JSON, CSV or
   Excel.

**Treat rows with an `error` field as the failure signal** — the run stays green
even when an account could not be read.

#### From the API

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

Watching a list of accounts for breaking posts, with the JavaScript client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('simple.actors/x-profile-posts').call({
    handles: ['NASA', 'WHO', 'espn'],
    onlyPostsNewerThan: '30 minutes',   // shorter than the gap between runs
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const row of items) {
    if (row.error) { console.warn(row.handle, row.error); continue; }
    if (row.isPinned) continue;          // a pin can be years old
    console.log(row.authorHandle, row.createdAt, row.text);
}
```

### Use cases

- **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.

### Usage notes

#### 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.

#### 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 | Run status |
|---|---|---|
| `not_found` | No such account | Succeeds |
| `not_available` | Suspended, withheld in this region, or otherwise restricted | Succeeds |
| `protected` | Posts are visible only to approved followers, so a login would be needed | Succeeds |
| `bad_input` | The entry was never a usable handle or profile URL | Succeeds |
| `window_too_wide` | `onlyPostsNewerThan` reaches further back than this run can see — see below | Succeeds |
| `read_failed` | X did not serve a readable page after several attempts | Succeeds |

**No unreadable account fails the run** — not one of them, and not all of them
at once. Whether it is an answer about the account (missing, suspended,
protected, mistyped) or a read that did not happen (X did not serve the page),
the run finishes as SUCCEEDED with the reason in a row. The run's status message
counts both kinds: how many accounts were read, how many answered with an
`error` row, and how many could not be reached.

**So check the rows, not the run status.** If you are scheduling this, treat any
row with an `error` field as the failure signal — a green run can still contain
accounts that were not read, and a run status of SUCCEEDED does not by itself
mean every account came back.

An empty dataset still means exactly one thing — read, nothing new — because
every failure leaves a row.

#### `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 that account gets a `window_too_wide` row 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.

### FAQ

**Is scraping X (Twitter) legal?**
This Actor reads only what X already renders for logged-out visitors — it does
not log in, use cookies, or reach protected accounts. X's Terms of Service
restrict automated collection, so check the platform's ToS and your own
obligations, particularly around personal data, before using it.

**Do I need an X API key or a developer account?**
No. There is no key, no login, no session and no account of yours involved —
nothing to get rate-limited or suspended.

**Can I get more than 5 posts per profile?**
No. Five is what X renders to a logged-out reader, not a setting, and no option
raises it. This Actor is built to check *many* accounts *often*; for one
account's deep history it is the wrong tool.

**Does it support pagination?**
There is no next page to request for a logged-out reader, so no. Run the Actor
on a schedule with `onlyPostsNewerThan` to follow accounts over time.

**Can it read protected (private) accounts?**
No. A protected account comes back as a row saying so, and the run still
succeeds.

**Why is the first row not the account's newest post?**
Pinned posts are the trap — X shows them first whatever their age. They are
flagged `isPinned`, and a pin is excluded from the reach calculation for
`onlyPostsNewerThan` so an old pin cannot make any window look covered.

**Why did I get a `window_too_wide` row instead of posts?**
Because the window reached further back than the five posts a run can see, so
returning what was found would have read as "this is everything since then".
Use a shorter window, or run more often.

**Why did my run succeed when an account was not read?**
By design. Every account that cannot be read comes back as a row with an `error`
field — whether that is an answer about the account (suspended, protected,
nonexistent, never a valid handle) or X not serving the page on the day — and
none of them turns the run red. Treat rows with an `error` field as the failure
signal rather than the run status, and read the run's status message for the
counts.

**Do I save money by asking for fewer posts?**
No. The charge is per profile, so a profile costs the same whether five posts
come back or one.

### 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.

## `outputFormat` (type: `string`):

**One row per post** (default) gives every post its own row, with the full account record repeated on each — the shape to pipe into a warehouse, and what existing code reading this Actor expects. **One row per account** gives each account a single row: the profile at the top level (handle, name, bio, follower and following counts, profile picture and banner, website, location, verification and badges), and its latest posts under `posts` with all 26 of these fields — id, type, url, twitterUrl, text, createdAtIso, timestamp, likeCount, retweetCount, replyCount, quoteCount, bookmarkCount, viewCount, isReply, isRetweet, isQuote, isPinned, isSensitive, lang, hashtags, mentions, urls, **media** (the pictures and video, so you do not need the other format for them), imageCount, videoCount and authorUserName. Same price either way; the row is a fraction of the size, and it is the one to pick when you want an account's details rather than a stream of posts.

## `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, an account whose window reaches further back than its latest posts do comes back as a window\_too\_wide error row rather than a partial answer — 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": [
    "NASA"
  ],
  "outputFormat": "posts",
  "maxPosts": 5,
  "proxy": {
    "useApifyProxy": true
  },
  "includeRaw": false,
  "onlyPostsNewerThan": "3 days"
}
```

# Actor output Schema

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

One row per post with its headline numbers.

## `accounts` (type: `string`):

One row per account — for runs using the row-per-account output format.

## `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/rQDuRNBhvJDIHACL6/openapi.json
