# Truth Social Scraper | Profiles, Posts & Search (`abotapi/truthsocial-com-scraper`) Actor

Scrape public Truth Social profiles, posts, media, metrics, author data, and complete source objects using profile URLs, post URLs, or search queries.

- **URL**: https://apify.com/abotapi/truthsocial-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Social media, News, Developer tools
- **Features:** Limited permissions
- **Stats:** 12 total users, 7 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.70 / 1,000 dataset items

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

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

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

## How to integrate an Actor?

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

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

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

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

# README

## Truth Social Scraper: Posts, Profiles, Engagement Metrics & Media

Truth Social Scraper turns Truth Social into structured data. Point it at public profile, group or post URLs and get normalized posts, replies, profile metadata, media and engagement metrics, plus the complete source objects so no public field is lost. Resume an interrupted pull without being billed twice for posts you already collected, or schedule it and get only what's new, changed or gone since the last run. Export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **URLs or handles, any shape.** Add public profile, group or post URLs to the same list -- including the `/with_replies` and `/media` profile tabs and public group feeds (`/group/<name>`) -- or skip URLs entirely and list plain `@handle` usernames; **Profile content** then picks profile metadata only, posts only, replies only, or both, per run.
- **Filters that save you money.** Keep only posts with media, drop reposts, keep only posts newer than a date (paging stops as soon as it reaches older posts), or continue backwards from a given post.
- **Rich, normalized output across three dataset views.** Convenience fields for content, engagement (likes, ReTruths, replies, upvotes and downvotes), author metadata, media, quoted posts, reposts and group posts -- split into `posts`, `profiles` and `changes` dataset views, plus the complete source account/status object so nothing nested is dropped.
- **Resume and scheduled monitoring, without re-paying.** Point a new run at a previous run or dataset ID and it skips every post already saved there, or turn on Incremental mode to classify each post NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED against a remembered baseline so a recurring run reports only what changed.
- **Fails loudly, not silently.** A run where every request is refused stops with a clear error instead of billing you for a "no data" result; a genuinely empty match still succeeds and pushes nothing.
- **Send results onward.** Optional MCP connectors push a summary of each record into Notion, Linear, Airtable or Apify as soon as the scrape finishes.

### Use Cases

- **Political and public-figure monitoring:** track a public account's posts, edits and removed content over time.
- **Media and journalism:** pull post content, media and engagement metrics for reporting and fact-checking.
- **Brand and reputation monitoring:** watch a brand or organization's Truth Social activity on a schedule.
- **Academic and social research:** build datasets of public posts, engagement patterns and account metadata.
- **Content archiving:** keep a running record of a profile's posts, including ones later edited or removed.
- **Competitive and market research:** monitor announcements from organizations or public figures as they post.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `recordType` | `"post"` (also `"reply"`, or `"profile"` for profile records) |
| `sourceQuery` | `"https://truthsocial.com/@exampleuser"` (the start URL the row came from; a plain handle from **Usernames** shows as its profile URL) |
| `postId` / `id` | `"113456789012345678"` / `"113456789012345678"` (`postId` is the reposted source id for a repost, else the status id; `id` is the status's own id, differing from `postId` only for a repost) |
| `postUrl` | `"https://truthsocial.com/@exampleuser/113456789012345678"` |
| `content` / `contentHtml` | `"Sample post text goes here."` / `"<p>Sample post text goes here.</p>"` |
| `createdAt` / `editedAt` / `language` | `"2026-08-01T12:00:00.000Z"` / `null` (set when the author edited the post) / `"en"` |
| `repliesCount` / `reTruthsCount` / `likesCount` | `42` / `15` / `320` |
| `upvotesCount` / `downvotesCount` | `320` / `4` (Truth Social's own up/down tally, separate from `likesCount`) |
| `quotesCount` | `null` -- Truth Social does not publish a quote count, so this column is always empty; it is kept so existing integrations keep working |
| `isReblog` / `rebloggedByAcct` / `rebloggedFromUrl` | `false` / `null` / URL of the original post when the row is a repost, else `null` |
| `mediaUrls` / `hasMedia` / `mediaCount` / `mediaTypes` | `["https://static-assets.truthsocial.com/sample.jpg"]` / `true` / `1` / `["image"]` |
| `mediaAttachments` | one entry per image or video: `id`, `type`, `url`, `previewUrl`, `textUrl`, `description` |
| `tags` / `mentions` | `["sample"]` / mentioned accounts on the post |
| `poll` | poll options and vote counts, when present |
| `cardTitle` / `cardUrl` | link-preview card title and URL, when present |
| `authorUsername` / `authorDisplayName` / `authorAvatarUrl` / `authorVerified` / `authorCreatedAt` | `"exampleuser"` / `"Example User"` / the author's avatar, verified badge and account creation date |
| `authorFollowersCount` / `authorFollowingCount` / `authorPostsCount` | the author's follower, following and post counts at scrape time |
| `pinned` / `sponsored` | `false` / `false` |
| `quoteId` / `quotedPostUrl` / `quotedPostContent` / `quotedAuthorUsername` | id of the quoted post, or `null`; the quoted post's URL, plain text and author, when the post quotes another one |
| `inReplyToUsername` | handle of the account a reply answers, when the site includes it |
| `groupId` / `groupName` / `groupUrl` | the Truth Social group a post was made in, else `null` |
| `title` | the post title some group posts carry, else `null` |
| `version` / `editable` | the post's edit version and whether it can still be edited |
| `rawStatus` | the complete source status object |

Profile records (`recordType` `"profile"`, returned only when **Profile content** is `profileOnly`) carry `sourceQuery`, `userId`, `username`, `acct`, `displayName`, `bio` and `bioHtml`, `url`, `avatar` and `header` images, `followersCount`, `followingCount`, `statusesCount`, `createdAt`, `lastStatusAt`, `verified`, `locked`, `bot`, `website`, `location`, `premium`, `isGroupAccount`, `fields`, `emojis` and the complete `rawAccount` object.

The dataset ships four views: `overview` (every column, mixing post and profile rows), `posts` (post/reply columns only), `profiles` (bio, counts, website, location, premium, verified) and `changes` (`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`, `postId`, `postUrl`, `content`, `createdAt`, populated only in Incremental mode). Switch views from the dataset's **Choose columns** dropdown on the run's Output tab.

### How to Use

1. Add one or more Truth Social profile, group or post URLs under **Start here**, or list plain handles under **Usernames** instead -- mix both if you like. Leave both empty and the actor scrapes `@realDonaldTrump` by default.
2. For profile URLs, pick **Profile content**: profile only, posts only, replies only, or both. A pasted `.../with_replies` or `.../media` URL (see the shapes table below) applies that tab's content for just that URL, regardless of this setting.
3. Optionally narrow profile and group posts under **Filters**: **Only posts with media**, **Include reposts** (on by default), **Posts newer than** (a date such as `2026-10-01` or a period such as `7 days`) and **Start from post ID** (return only posts older than that post). Filters apply to profile and group URLs, never to a pasted post URL.
4. Set **Max items** to cap run size and cost; optionally set **Max posts/replies per profile** or **Max pages**.
5. For recurring monitoring, turn on **Incremental changes for scheduled runs** under Resume and recurring updates, then click **Start**.

**A single profile, posts and replies:**

```json
{
  "startUrls": [{ "url": "https://truthsocial.com/@realDonaldTrump" }],
  "profileContentType": "postsAndReplies",
  "maxItems": 50
}
```

**Individual posts by URL:**

```json
{
  "startUrls": [
    { "url": "https://truthsocial.com/@exampleuser/113456789012345678" },
    { "url": "https://truthsocial.com/@anotheruser/113456789012345999" }
  ],
  "includeRaw": false
}
```

**Only the last 7 days of a profile's own posts with media, no reposts:**

```json
{
  "startUrls": [{ "url": "https://truthsocial.com/@exampleuser" }],
  "onlyMedia": true,
  "includeReblogs": false,
  "postsNewerThan": "7 days",
  "maxItems": 200
}
```

**Recurring incremental monitoring:**

```json
{
  "startUrls": [{ "url": "https://truthsocial.com/@exampleuser" }],
  "profileContentType": "postsOnly",
  "incrementalMode": true,
  "emitExpired": true,
  "stateKey": "exampleuser-posts"
}
```

#### Supported URL shapes

A leading `www.` is accepted, but the `https://` (or `http://`) scheme itself must be present -- Apify's own input validator rejects a pasted URL with no scheme before this actor ever runs, so a bare `truthsocial.com/@handle` is not a valid **Start here** entry (use **Usernames** for that instead). Tracking params (`?utm_source=...`) are ignored either way. Every row below is one of the prefilled **Start here** examples.

**Default when you omit the field:** leaving **Start here** (`startUrls`) out of the input now scrapes one example profile, `https://truthsocial.com/@realDonaldTrump`, instead of nothing. Click **Restore example input** in the Console to load one example of every shape below. If you only want specific accounts, send your own URLs or usernames; the default applies only when both **Start here** and **Usernames** are empty -- if you send `usernames` and omit `startUrls` (the platform then fills in the example profile), the example profile is skipped and only your usernames are scraped; if you list your own URLs next to `usernames`, both are scraped.

| Shape | Example | Resolves to |
| --- | --- | --- |
| Profile | `https://truthsocial.com/@realDonaldTrump` | that account's posts/replies, per **Profile content** |
| Profile -- posts & replies tab | `https://truthsocial.com/@realDonaldTrump/with_replies` | the account's posts and replies, even if **Profile content** is set to Posts only |
| Profile -- media tab | `https://truthsocial.com/@realDonaldTrump/media` | only posts/replies from that account that carry media |
| Post (canonical) | `https://truthsocial.com/@realDonaldTrump/posts/116813202769287423` | that single post |
| Post (bare id) | `https://truthsocial.com/@realDonaldTrump/116813202769287423` | the same single post |
| Post (alternate link form) | `https://truthsocial.com/users/realDonaldTrump/statuses/116813202769287423` | the same single post |
| Group | `https://truthsocial.com/group/wine-with-devin-nunes` | the public group's posts, newest first, as `post` rows with `groupId`/`groupName`/`groupUrl` filled in (**Profile content** does not apply; the **Filters** do) |
| Group post | `https://truthsocial.com/group/wine-with-devin-nunes/posts/117121586897225540` | that single post made in the group |
| `www.` prefix (scheme still required) | `https://www.truthsocial.com/@realDonaldTrump` | the same as the matching profile/post shape above |
| Not supported (logged and skipped) | tag/hashtag pages (`/tags/...`), the groups directory (`/groups`), search URLs (`/search?...`), the trending page, `links.truthsocial.com` short links, a bare `/`, or any other host | nothing -- the run logs a warning naming the URL and moves on, it is never misrouted to the wrong profile |

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/truthsocial-com-scraper").call(run_input={
    "startUrls": [{"url": "https://truthsocial.com/@realDonaldTrump"}],
    "maxItems": 50,
})
for post in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(post["postId"], post["content"][:80])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/truthsocial-com-scraper').call({
  startUrls: [{ url: 'https://truthsocial.com/@realDonaldTrump' }],
  maxItems: 50,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted pull: paste its run or dataset ID and the actor skips every post it already saved there. Truth Social's timeline is newest-first, so a resumed profile walk stops paginating as soon as it catches up, so it costs less than a full re-scrape, not just returns fewer rows. An id that cannot be resolved to a run or dataset fails the run immediately, before anything is collected or billed.
- **Incremental mode** (`incrementalMode`) is for running the same profile/post set again and again. Each post is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED`. `EXPIRED` is produced only once a run completely scans every tracked profile with `emitExpired` on; a run cut short by Max items, Max pages, or combined with Resume skips EXPIRED detection that run and keeps the previous state as-is (and is itself budgeted to the run's remaining Max items headroom, so a very large backlog of missing posts is never tombstoned in one over-budget run -- the rest is picked up by a later run). `REAPPEARED` only appears for a post that an earlier `emitExpired` run had already marked missing and that then shows up again on any later run that finds it, complete scan or not; it is not produced on a normal or first run. `stateKey` names or shares one monitoring baseline; left empty, a key is derived automatically from `startUrls` and `profileContentType`. Combining Incremental mode with Resume is allowed only on the very first run for a state key; once that key has tracked state, a run using both fails instead of mixing the two mechanisms. A `profileOnly` profile row is tracked too (its own counters, like followers, never cause a false `UPDATED` on their own). On post rows the author's follower/following/post counts, avatar and verified badge, and the `editable` flag (which switches off by itself when the edit window closes) never cause an `UPDATED` on their own either -- a real edit still shows up through `content`, `editedAt` and `version`. A run using any of the **Filters** only looks at part of each profile, so it never marks posts `EXPIRED`.
- **Ignore fields when detecting changes** (`ignoreFieldsForChanges`) excludes extra field names from change detection, on top of what's already excluded by default: a poll's `votes_count`/`voters_count`, each poll option's own `votes_count`, and a closed poll's `expired` flag never trigger a false `UPDATED` by themselves, at any nesting depth -- they drift on their own as people keep voting, not because the post changed. The author's avatar and verified badge (`authorAvatarUrl`, `authorVerified`) and the start URL a row came from (`sourceQuery`) are left out of the diff too: they describe the author or your input, not the post. Ignored fields are still returned in the output; only the change-detection diff skips them. Add a field name here if something else in your own setup turns out to be noisy.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed summary of each record, a title plus its key fields flattened to plain markdown text, not the full JSON. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created; if it's left blank for a Notion connector, that export is skipped with a warning. Other connectors ignore this field.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave `mcpConnectors` empty to skip export entirely.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `startUrls` | array | `[{"url": "https://truthsocial.com/@realDonaldTrump"}]` (prefill: one example per supported shape) | Public Truth Social profile, group or post URLs, mixed in the same list. See [Supported URL shapes](#supported-url-shapes) for every accepted form. |
| `usernames` | array | `[]` | Optional. Plain handles, with or without a leading `@`, merged into the same run as `startUrls` -- each one becomes a profile fetch. When **Start here** still holds only the example profile, that example is skipped. |
| `profileContentType` | string | `postsAndReplies` | For profile URLs: `profileOnly`, `postsOnly`, `repliesOnly`, or `postsAndReplies`. Ignored for post and group URLs. |
| `maxPostsPerProfile` | integer | `0` | No per-profile cap by default; the run still stops at Max items. Fewer rows are returned when a profile has fewer public matching records. |
| `onlyMedia` | boolean | `false` | Keep only posts and replies with their own image or video (as on the profile's Media tab; reposts are left out). |
| `includeReblogs` | boolean | `true` | Turn off to drop reposts and keep only the profile's own posts and replies. |
| `postsNewerThan` | string | none | Keep only posts published on or after this date (`2026-10-01`, `2026-10-01T12:00:00Z`) or within a period counted back from the run start (`7 days`, `12 hours`, `2 weeks`). Paging stops as soon as a profile reaches older posts. A repost counts from when it was reposted. An unreadable value fails the run before anything is fetched or billed. |
| `startFromId` | string | none | Return only posts older than this post: a post ID or post URL. Anything else fails the run before anything is fetched or billed. |
| `maxItems` | integer | `20` (also prefilled as `20`) | Maximum matching rows saved across the whole run. |
| `maxPages` | integer | `0` | No page limit by default; the run stops at Max items. |
| `resumeFromRunId` | string | none | Paste a previous run ID or dataset ID to continue a large pull without re-collecting or re-billing posts already saved there. |
| `incrementalMode` | boolean | `false` (also prefilled as `false`) | Turn on for daily or recurring monitoring; later runs return only NEW and UPDATED posts by default. REAPPEARED only occurs after an earlier run with Emit expired posts on has marked a post missing. |
| `stateKey` | string | none | Names or shares an incremental-mode baseline; left empty, a key is derived automatically from `startUrls` and `profileContentType`. |
| `peek` | boolean | `false` (also prefilled as `false`) | Incremental mode only: report what changed since the last run without saving the new baseline, so the next run reports the same changes again. |
| `emitUnchanged` | boolean | `false` (also prefilled as `false`) | Also return (and bill) posts that have not changed since the last incremental run. |
| `emitExpired` | boolean | `false` (also prefilled as `false`) | Also return (and bill) posts no longer found; only produced once a run has completely scanned every tracked profile, not when Max items, Max pages, or Resume cut it short. Budgeted to the run's remaining Max items headroom. |
| `includeRaw` | boolean | `true` | Keeps the complete source account/status object on every record. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Extra field names to exclude from `UPDATED` change detection, in addition to the built-in poll vote/voter/option-vote counters and the `expired` flag. Matches a field name at any nesting depth. Ignored fields are still returned in the output. |
| `countryCode` | string | `US` | Preferred connection country; US is recommended for Truth Social. |
| `proxyConfiguration` | object | Apify Residential (`useApifyProxy: true`, group `RESIDENTIAL`; same as the prefill) | Connection settings. A working connection is required; a run configured with no connection at all is rejected outright and fails. |
| `mcpConnectors` | array | none | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which records are created. Required only when exporting to Notion. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run; does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "recordType": "post",
  "sourceQuery": "https://truthsocial.com/@exampleuser",
  "postId": "113456789012345678",
  "id": "113456789012345678",
  "uri": "https://truthsocial.com/@exampleuser/113456789012345678",
  "postUrl": "https://truthsocial.com/@exampleuser/113456789012345678",
  "content": "Sample post text goes here.",
  "contentHtml": "<p>Sample post text goes here.</p>",
  "createdAt": "2026-08-01T12:00:00.000Z",
  "editedAt": null,
  "language": "en",
  "repliesCount": 42,
  "reTruthsCount": 15,
  "likesCount": 320,
  "favouritesCount": 320,
  "quotesCount": null,
  "inReplyToId": null,
  "sensitive": false,
  "visibility": "public",
  "isReblog": false,
  "rebloggedByAcct": null,
  "mediaUrls": [],
  "tags": ["sample"],
  "mentions": [],
  "poll": null,
  "cardTitle": null,
  "cardUrl": null,
  "authorId": "900000000000000001",
  "authorUsername": "exampleuser",
  "authorAcct": "exampleuser",
  "authorDisplayName": "Example User",
  "authorUrl": "https://truthsocial.com/@exampleuser",
  "upvotesCount": 320,
  "downvotesCount": 4,
  "pinned": false,
  "sponsored": false,
  "quoteId": null,
  "authorAvatarUrl": "https://static-assets.truthsocial.com/avatar.jpg",
  "authorFollowersCount": 1200,
  "authorFollowingCount": 85,
  "authorPostsCount": 640,
  "authorVerified": false,
  "authorCreatedAt": "2022-02-01T00:00:00.000Z",
  "rebloggedFromUrl": null,
  "mediaCount": 0,
  "hasMedia": false,
  "mediaTypes": [],
  "mediaAttachments": [],
  "quotedPostUrl": null,
  "quotedPostContent": null,
  "quotedAuthorUsername": null,
  "groupId": null,
  "groupName": null,
  "groupUrl": null,
  "inReplyToUsername": null,
  "title": null,
  "version": "1",
  "editable": false,
  "rawStatus": {},
  "scrapedAt": "2026-09-29T00:00:00.000Z"
}
```

### Plan Requirement

Truth Social blocks plain datacenter and direct connections to its data endpoints (the site answers them with an access-denied page), so a datacenter-only setup does not work and the actor does not rely on one. It defaults to Apify's Residential proxy group and keeps the country code set to `US`, which the site responds to most reliably; when the residential connection is refused, requests are retried over a backup connection. Residential access is the recommended setting, but it is not a guarantee that every page loads on every run.

A **free Apify plan does not include residential proxies**. On a free plan, the run logs a one-time warning and automatically continues on a backup connection instead of stopping -- most free-plan runs still complete, though some pages may come back unavailable if the backup connection can't reach them either.

If a run can't read a single page at all, it fails outright instead of returning an empty dataset, with one of three messages depending on what happened:

- the account's plan has no proxy access at all (no paid plan, no custom proxy URLs supplied);
- a free plan whose backup connection also could not load the requested pages;
- any other connection problem (a paid plan, or an unrecognized entitlement) where every request failed.

Upgrading to a plan that includes residential proxies, or supplying your own proxy URLs, resolves all three.

### FAQ

#### How much does it cost?

You pay a small per-run start fee plus a per-record fee for each row saved to the dataset. The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of any run.

#### Is it legal to scrape Truth Social?

This actor collects only publicly available profile and post data. You are responsible for how you use it: follow Truth Social's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Some public social media records may still contain personal data, so handle results accordingly.

#### Can I get only new or changed posts on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental changes for scheduled runs**. Each run then returns only NEW and UPDATED posts by default (plus REAPPEARED once an earlier run with Emit expired posts on has marked one missing); UNCHANGED ones are suppressed and not billed unless `emitUnchanged` is on.

#### What's the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted run using a pasted run or dataset ID. **Incremental mode** remembers its own baseline and is meant for running the same profile/post set repeatedly on a schedule. Combining the two is allowed only the first time you turn Incremental mode on for a state key; once that key has tracked state, a run using both fails instead of mixing the two mechanisms.

#### Why did my run fail instead of returning an empty dataset?

If every request to Truth Social is refused, the run fails loudly with a message naming the reason instead of billing you for a "no data" result: no proxy access on this account's plan at all, a free plan whose backup connection also couldn't load the requested pages, or a general connection problem on any other plan. On a free plan specifically, a one-time warning is logged when residential proxies are unavailable and the run automatically continues on a backup connection -- the failure above only happens if that backup connection ALSO can't reach the requested pages. Upgrading to a plan with residential proxies, or supplying your own proxy URLs, resolves it.

If the site DID answer but nothing matched your input (for example, an account or post that doesn't exist), the run still succeeds with an empty dataset and reports why on the run's status message.

#### Can it scrape search results, trending posts, hashtags, groups or comments?

Groups, yes: paste a public group URL (`https://truthsocial.com/group/<name>`) and you get the group's posts, newest first, with `groupId`, `groupName` and `groupUrl` filled in. Search, trending, hashtags and comments, no: Truth Social only serves those to signed-in accounts -- search, trending and the comment threads under a post answer anonymous requests with an authorization error, and hashtag feeds with an empty list. This actor works without any login, so pasted search, tag or trending URLs are skipped with a warning rather than half-scraped.

#### Is there a clean-text option?

You get both on every post: `content` is already plain text, and `contentHtml` keeps the original HTML. There is no "include muted" option either, because muting only exists for a signed-in account and never applies to anonymous scraping.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push a summary of each record into Notion, Linear or Airtable as soon as the run finishes.

### 🔗 Want more social media data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>📱 <a href="https://apify.com/abotapi/lemon8-profile-scraper"><b>Lemon8 Profile Scraper</b></a><br>Scrape Lemon8 user profiles with automated multi-profile discovery. Extract profile data...</td><td>📱 <a href="https://apify.com/abotapi/lemon8-discover-scraper"><b>Lemon8 Search Scraper</b></a><br>Scrape Lemon8 posts by keyword across multiple regions. Extract posts, images, videos...</td></tr>
<tr><td>📱 <a href="https://apify.com/abotapi/lemon8-feeds-scraper"><b>Lemon8 Feed Scraper</b></a><br>Scrape Lemon8 feeds across 22 categories and 10+ regions. Extract posts, images, videos...</td><td>📱 <a href="https://apify.com/abotapi/mastodon-social-scraper"><b>Mastodon</b></a><br>From $1/1K. Scrape trending Mastodon profiles and related posts from any Mastodon...</td></tr>
<tr><td>📱 <a href="https://apify.com/abotapi/lemon8-media-scraper"><b>Lemon8 Media Scraper</b></a><br>Lightweight Lemon8 scraper for extracting image and video URLs from posts, with optional...</td><td>📱 <a href="https://apify.com/abotapi/reddit-scraper"><b>Reddit⚡ Lightning</b></a><br>From $1/1K. Scrape Reddit posts, comments, and media from any subreddit or user profile -...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/truthsocial-com-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

Public Truth Social profile, group or post URLs, any shape the site itself links to: a profile (https://truthsocial.com/@handle), its posts-and-replies or media tab (.../with_replies, .../media), a public group (https://truthsocial.com/group/<name>, returns the group's posts), or a post (.../@handle/posts/<id>, .../@handle/<id>, .../users/<handle>/statuses/<id>, or a group post .../group/<name>/posts/<id>). A leading www. is accepted, but the https:// (or http://) scheme itself is required here -- Apify's own URL validator rejects a pasted link with no scheme before this Actor ever sees it. For a bare handle with no scheme at all, use Usernames below instead. Tag, search and trending pages, the groups directory and short links (links.truthsocial.com) need a signed-in account or a redirect and are skipped with a warning -- see the README's URL shapes table.

## `usernames` (type: `array`):

Optional. Plain Truth Social handles, with or without a leading "@" (for example realDonaldTrump or @realDonaldTrump). Each one is turned into a profile fetch and merged into the same run as Profile or post URLs above -- use whichever is more convenient, or both. If Profile or post URLs holds only the example profile, it is skipped and just your usernames are scraped.

## `profileContentType` (type: `string`):

For profile URLs, choose whether to return profile metadata only, posts only, replies only, or both posts and replies. A /with_replies or /media URL tab in the input overrides this setting. Ignored for post and group URLs (a group URL always returns the group's posts).

## `maxPostsPerProfile` (type: `integer`):

Leave 0 for no per-profile cap; the run stops at Max items. Fewer rows are returned when the profile has fewer public matching records.

## `onlyMedia` (type: `boolean`):

Keep only posts and replies that carry their own image or video, like the profile's Media tab. Reposts are left out, as on the site.

## `includeReblogs` (type: `boolean`):

On by default. Turn off to drop reposts of other people's posts and keep only the profile's own posts and replies.

## `postsNewerThan` (type: `string`):

Optional. Keep only posts published on or after this date (for example 2026-10-01), or within a period counted back from the run start (for example 7 days). The run stops paging a profile as soon as it reaches older posts, so you only pay for what you keep. A repost counts from the moment it was reposted.

## `startFromId` (type: `string`):

Optional. Return only posts OLDER than this post, to continue backwards through a long history. Paste a post ID (the number at the end of a post URL) or the post URL itself.

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

The only finite default cap. Maximum matching rows to save across the whole run; fewer rows are returned when selected inputs have fewer public matches.

## `maxPages` (type: `integer`):

No page limit by default. Leave 0 for unlimited; the run stops at Max items.

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

Keep complete source account/status objects so no nested public field is dropped.

## `countryCode` (type: `string`):

Preferred proxy country. US is recommended for Truth Social.

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

Residential proxies are recommended. Basic shared or no-proxy runs can be blocked more often.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large profile pull without returning or charging for posts already collected there. Use this after an interrupted run, or when continuing a pull in another run. For recurring monitoring of the same profiles, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching posts as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED posts. Turn on "Emit unchanged" or "Emit expired" only when you also want those posts returned (and billed). State is kept separately for each startUrls/profile content setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the profile/post URLs and content type settings -- different profile sets then never mix state with each other.

## `peek` (type: `boolean`):

Preview what changed since the last incremental run without saving the new baseline: the next run reports the same changes again. Off by default. Has no effect unless Incremental changes for scheduled runs is on.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return posts that have not changed since the last run, marked UNCHANGED. This returns -- and bills -- extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return posts that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned every tracked profile -- not when Max items capped it or when Resume was used. This returns -- and bills -- extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to exclude from UPDATED change detection, in addition to the built-in handling (a poll's vote/voter tallies, each poll option's own vote count, and a closed poll's expired flag never trigger a false UPDATED by themselves -- they drift on their own as people keep voting, not because the post changed; the author's avatar and verified badge and the start URL a row came from are also left out of the diff). Matches a field name at any nesting depth (for example inside poll or inside each item of media). Ignored fields are still returned in the output. Use this if another field in your setup turns out to be noisy.

## `mcpConnectors` (type: `array`):

Optionally send a compact summary into configured MCP connectors. The full dataset remains in Apify.

## `notionParentPageUrl` (type: `string`):

Required only when exporting to Notion.

## `maxNotifyListings` (type: `integer`):

Maximum items sent to each connector. Does not affect the Apify dataset.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://truthsocial.com/@realDonaldTrump"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/with_replies"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/media"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/posts/116813202769287423"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/116813202769287423"
    },
    {
      "url": "https://truthsocial.com/users/realDonaldTrump/statuses/116813202769287423"
    },
    {
      "url": "https://truthsocial.com/group/wine-with-devin-nunes"
    },
    {
      "url": "https://truthsocial.com/group/wine-with-devin-nunes/posts/117121586897225540"
    }
  ],
  "usernames": [],
  "profileContentType": "postsAndReplies",
  "maxPostsPerProfile": 0,
  "onlyMedia": false,
  "includeReblogs": true,
  "maxItems": 20,
  "maxPages": 0,
  "includeRaw": true,
  "countryCode": "US",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "peek": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

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

No description

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

No description

## `changes` (type: `string`):

No description

# 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://truthsocial.com/@realDonaldTrump"
        },
        {
            "url": "https://truthsocial.com/@realDonaldTrump/with_replies"
        },
        {
            "url": "https://truthsocial.com/@realDonaldTrump/media"
        },
        {
            "url": "https://truthsocial.com/@realDonaldTrump/posts/116813202769287423"
        },
        {
            "url": "https://truthsocial.com/@realDonaldTrump/116813202769287423"
        },
        {
            "url": "https://truthsocial.com/users/realDonaldTrump/statuses/116813202769287423"
        },
        {
            "url": "https://truthsocial.com/group/wine-with-devin-nunes"
        },
        {
            "url": "https://truthsocial.com/group/wine-with-devin-nunes/posts/117121586897225540"
        }
    ],
    "usernames": [],
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "peek": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/truthsocial-com-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 = {
    "startUrls": [
        { "url": "https://truthsocial.com/@realDonaldTrump" },
        { "url": "https://truthsocial.com/@realDonaldTrump/with_replies" },
        { "url": "https://truthsocial.com/@realDonaldTrump/media" },
        { "url": "https://truthsocial.com/@realDonaldTrump/posts/116813202769287423" },
        { "url": "https://truthsocial.com/@realDonaldTrump/116813202769287423" },
        { "url": "https://truthsocial.com/users/realDonaldTrump/statuses/116813202769287423" },
        { "url": "https://truthsocial.com/group/wine-with-devin-nunes" },
        { "url": "https://truthsocial.com/group/wine-with-devin-nunes/posts/117121586897225540" },
    ],
    "usernames": [],
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "peek": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/truthsocial-com-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 '{
  "startUrls": [
    {
      "url": "https://truthsocial.com/@realDonaldTrump"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/with_replies"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/media"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/posts/116813202769287423"
    },
    {
      "url": "https://truthsocial.com/@realDonaldTrump/116813202769287423"
    },
    {
      "url": "https://truthsocial.com/users/realDonaldTrump/statuses/116813202769287423"
    },
    {
      "url": "https://truthsocial.com/group/wine-with-devin-nunes"
    },
    {
      "url": "https://truthsocial.com/group/wine-with-devin-nunes/posts/117121586897225540"
    }
  ],
  "usernames": [],
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "peek": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/truthsocial-com-scraper --silent --output-dataset

```

## MCP server setup

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