# Mastodon Scraper - Trending Profiles, Posts & Replies (`abotapi/mastodon-social-scraper`) Actor

Scrape trending Mastodon profiles and related posts from any Mastodon instance. Extract usernames, bios, follower counts, avatars, profile fields, posts and thread replies. Supports nested profile reviews or flat review output for easier analysis.

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

## Pricing

from $1.00 / 1,000 results

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

## Mastodon Scraper: Trending Profiles, Posts and Replies

Mastodon Scraper turns any public Mastodon server into structured data. Get profiles with bios, follower counts and custom fields, their recent posts and the replies underneath them, or bulk posts collected by hashtag at volume. Discover accounts through currently trending posts and the public profile directory, or paste exact profile and post links to scrape only what you want. Export to JSON, CSV or Excel, or pull results through the API. Defaults to mastodon.social and works on any public Mastodon instance.

### Why This Scraper?

- **Three output shapes.** Profiles with reviews nested inside, a flat stream of reviews, or bulk keyword posts, so you pick the shape your pipeline needs.
- **Two discovery sources, merged.** Authors of currently trending posts and the active public directory, deduplicated automatically into one list.
- **Full conversation context.** Every post's reply thread is captured alongside it, tagged separately from the profile's own posts.
- **Deep history on demand.** Walk a single profile's post history, or a hashtag's public timeline, back through thousands of posts via automatic load-more pagination.
- **URL mode.** Paste exact profile or post links to scrape only the accounts and posts you choose.
- **Works on any public Mastodon server.** Point the instance setting at mastodon.social or any other server address.
- **Nothing is dropped from the author account.** Every post and reply keeps its author's complete upstream account object; the post's own fields are the mapped set documented below (a poll, application info, and a few link-card details are not carried through).

### Use Cases

- **Community and trend research:** track who and what topics are trending on a Mastodon server over time.
- **Social listening:** bulk-collect posts under a hashtag to study a topic or event at scale.
- **Account monitoring:** watch specific profiles for new posts and replies on a recurring schedule.
- **Fediverse research and journalism:** build datasets of public conversation threads for analysis.
- **Community growth tracking:** monitor follower counts and posting activity for chosen accounts.

### Data You Get

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

| Field | Example |
| --- | --- |
| `recordType` | `"review"` |
| `id` | `"112233445566778899"` |
| `reviewType` | `"post"` (also `"reply"`) |
| `sourceKeyword` | `"climate"` (keyword mode only; `null` otherwise) |
| `contentText` | `"Sharing our monthly update on the project roadmap."` |
| `createdAt` | `"2026-03-14T09:12:00.000Z"` |
| `language` | `"en"` |
| `url` | `"https://mastodon.example/@exampleuser/112233445566778899"` |
| `repliesCount` | `14` |
| `reblogsCount` | `37` |
| `favouritesCount` | `120` |
| `inReplyToId` | `null` |
| `isReblog` | `false` |
| `rebloggedByAcct` | `null` (set to the booster's handle when `isReblog` is true) |
| `tags` | `["climate", "sustainability"]` |
| `mediaAttachments` | `[]` (or the attached image/video objects) |
| `sensitive` | `false` |
| `authorAcct` | `"exampleuser"` |
| `authorDisplayName` | `"Example User"` |
| `authorUrl` | `"https://mastodon.example/@exampleuser"` |
| `account` | `{ ... complete upstream author object ... }` |

Profile records (profiles mode) carry `acct`, `username`, `displayName`, `bio`, `followersCount`, `followingCount`, `statusesCount`, `url`, `avatar`, `createdAt` and any custom profile fields the account has set, plus a nested `reviews` array (that profile's own posts and replies) and `reviewCount`. Review records also carry `contentHtml`, `editedAt`, `visibility`, `spoilerText`, `inReplyToAccountId`, `mentions` and link-card fields (`cardUrl`, `cardTitle`) alongside the columns above. Every profile and review record additionally keeps the complete upstream Mastodon account object under `account`, so nothing the source API returns is ever dropped.

Every post or reply record also carries flat convenience fields: `uri`, `text`, `author` (id, username, acct, displayName, avatar, followersCount, followingCount, statusesCount, url), `engagement` (replies, reblogs, favourites), `media` (type, url, previewUrl), `isBoost`, `isReply`, `source`, `instance` and `crawledAt`. They are additive and never trigger an UPDATED change in incremental mode.

### How to Use

1. Pick a **mode**: `profiles` (nested reviews under each profile), `reviews` (flat, one record per post or reply), or `keyword` (bulk posts by topic).
2. For profiles or reviews mode, pick a **profile source** (trending, directory, or both), or paste profile and post URLs instead to scrape exactly those.
3. Set the limits for your mode (max profiles, posts per profile, replies per post, or max posts for keyword mode) and turn **Include replies** on or off.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Trending profiles with their reviews (default):**

```json
{
  "mode": "profiles",
  "profileSource": "both",
  "includeReplies": true,
  "maxProfiles": 10,
  "maxReviewsPerProfile": 5,
  "maxRepliesPerPost": 10
}
```

**Reviews only, capped, no replies (faster and cheaper):**

```json
{
  "mode": "reviews",
  "profileSource": "trending",
  "includeReplies": false,
  "maxProfiles": 20,
  "maxReviewsPerProfile": 10,
  "maxReviews": 200
}
```

**Bulk posts by keyword (high volume, load-more pagination):**

```json
{
  "mode": "keyword",
  "keywords": ["news", "art"],
  "includeReplies": false,
  "maxPosts": 5000
}
```

**URL mode (specific profiles).** The `urls` field now defaults to a short list with one example of every supported URL shape, so the input form shows what is supported. Default change: that example list is scraped only when Output mode is set to `urls`; if you leave the list unedited in profiles, reviews or keyword mode it is ignored and the run behaves as before. Any list you edit or paste is always used (keyword mode never uses URLs). URLs that match no supported shape are skipped with a warning in the run log, never silently misread.

| URL shape | Example | Result |
| --- | --- | --- |
| Profile | `https://mastodon.social/@Mastodon` | that profile |
| Profile replies tab, paginated tab | `/@Mastodon/with_replies`, `/@Mastodon/with_replies?max_id=1` | that profile |
| Profile RSS | `/@Mastodon.rss` | that profile |
| ActivityPub profile | `/users/Mastodon` | that profile |
| Web-client profile | `/web/@Mastodon` | that profile |
| Profile on another instance | `https://fosstodon.org/@fosstodon` | that profile |
| Post | `/@Mastodon/117466985005297414` | that post and its author |
| Post embed | `/@Mastodon/117466985005297414/embed` | that post |
| ActivityPub post | `/users/Mastodon/statuses/117466985005297414` | that post |
| Web-client post | `/web/statuses/117466985005297414` | that post |
| Tag page | `/tags/news` | recent posts with the tag (at most `maxProfiles` posts per tag URL; `since`, `until` and `sortBy` apply) |
| Explore page | `/explore`, `/explore/posts` | authors of trending posts |
| Profiles directory | `/directory` | active directory profiles |
| JSON API | `/api/v1/accounts/lookup?acct=`, `timelines/tag/`, `trends/statuses`, `directory`, `statuses/ID/context` | mapped to the matching shape above |

Posts on another instance and other explore tabs are skipped with a warning. Keywords and the `accounts` field accept plain names (a leading `#` or `@` is ignored); a keyword or account with no match is reported with a warning.

**URL mode (specific profiles and posts):**

```json
{
  "mode": "urls",
  "urls": [
    "https://mastodon.social/@Mastodon",
    "https://mastodon.social/@Mastodon/117466985005297414"
  ],
  "includeReplies": false,
  "maxReviewsPerProfile": 3
}
```

An edited list also works in profiles or reviews mode without setting `mode` to `urls`.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/mastodon-social-scraper").call(run_input={"mode": "keyword", "keywords": ["news"], "maxPosts": 100})
for post in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(post["authorAcct"], post["contentText"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/mastodon-social-scraper').call({ mode: 'keyword', keywords: ['news'], maxPosts: 100 });
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

Two different things, both optional:

- **Resume from a previous run** (`resumeFromRunId`): paste a previous run or dataset ID to continue an interrupted crawl. The actor repeats the same search and skips every record it already collected there, so you are not billed twice for the same post or profile. It walks to the same depth as the original run rather than digging further into history, so resume soon after an interruption for the most complete pickup of anything still missing.
- **Incremental mode** (`incrementalMode`): turn this on for recurring monitoring of the *same* search (daily or weekly). The actor remembers its own baseline in a key-value store, keyed by mode, instance and your query settings (including `accounts`, `includeBoosts` and `since`/`until` when set; the `max*` depth limits are excluded, so two runs of the same search with different limits still share one baseline; no run or dataset ID to paste). The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED` and `REAPPEARED` records by default. Turn on `emitUnchanged` or `emitExpired` only if you also want those rows back, since both are extra billed items.

What is compared: every profile field and every post field. What is excluded from the comparison (but still returned): the engagement counters of posts nested inside a profile record, the embedded author's follower, following and post counters on a post record, list ordering, and any field you name in `ignoreFieldsForChanges`. `EXPIRED` is only produced after a run that reached the natural end of every scope, so it is never produced for explore (trending/directory) discovery, for runs that include a pasted explore, directory or tag URL, or when a cap, Resume or a failed fetch cut the run short, and replies never expire.

When incremental mode is on, every pushed record gains four fields: `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED`), `changedFields` (which fields differed from the last run), `firstSeenAt` and `lastSeenAt`.

`EXPIRED` rows (a profile or post that was present before but can no longer be found) are only produced once a run has scanned its whole tracked scope without being cut short by a cap, a failed page, an instance error, or Resume; a partial scan cannot tell "gone" apart from "not reached yet". Turning `emitExpired` on is also what starts tracking absence at all: with it off, a record that stops appearing simply keeps its last-known state instead of being marked gone, and a record can only be reported `REAPPEARED` after it was first marked gone by an earlier complete run that had `emitExpired` on. The actor does not distinguish a post the author deleted from one that disappeared for any other reason (moderation, an account closing, and so on); `EXPIRED` only means the record could no longer be found in a complete scan. In **explore mode** specifically, "complete scan" means every profile in that run's discovered top-N from the trending and directory listings was processed, not that every account on the instance was checked; since the trending list and the public directory's ranking can reshuffle between runs, an account that simply drops out of this run's top-N (while still existing) is indistinguishable from one that's genuinely gone, so `emitExpired` in explore mode can mark, and bill, `EXPIRED` rows for accounts that are still there. In **keyword mode**, a reply on a post whose parent turned out `UNCHANGED` this run is not re-fetched, so a run that completes normally can still tombstone replies it simply didn't re-check that run, which then bill again as `REAPPEARED` once the parent post next changes; this is a known limitation of keyword mode with Emit expired on, not a sign the reply is actually gone.

Use **State key** to name a monitoring campaign explicitly, or to intentionally share state across two differently-configured runs; leave it empty to let the actor derive one automatically so different searches never collide.

### 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 that happens after the scrape; the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. 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 your 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.

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

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `profiles` | `profiles` (nested reviews), `reviews` (flat), or `keyword` (bulk posts by topic). |
| `instanceUrl` | string | `https://mastodon.social` | Any public Mastodon server to read from. |
| `profileSource` | string | `both` | `trending`, `directory`, or `both`, merged and deduped. Used only when no URLs are given. |
| `onlyLocal` | boolean | `false` | Directory source: return only accounts local to the chosen instance. |
| `mode` (`urls`) | string | | `urls` scrapes exactly the listed URLs and returns the profiles output. |
| `urls` | array | one example per URL shape (see URL mode) | Mastodon URLs of any supported shape. An edited list applies in every mode except keyword. |
| `accounts` | array | (none) | Plain profile names (`Mastodon`, `@Mastodon`, `user@other.instance`). |
| `keywords` | array | (none; prefill: `["news"]`) | Topics to bulk-scrape in keyword mode, matched as tags (with or without a leading `#`). |
| `includeReplies` | boolean | `true` | Fetch the reply thread under each post. |
| `includeBoosts` | boolean | `true` | Keep posts a profile boosted from other accounts; turn off for own posts only. |
| `since` | string | none | Keep only posts created on or after this date (YYYY-MM-DD or ISO datetime). Applied to the fetched window per profile, pasted tag URL, keyword tag or pasted post URL. |
| `until` | string | none | Keep only posts created on or before this date (YYYY-MM-DD or ISO datetime). Applied to the fetched window per profile, pasted tag URL, keyword tag or pasted post URL. |
| `sortBy` | string | `newest` | `newest`, `oldest`, `mostFavourited`, `mostReblogged` or `mostReplied`, applied per profile, pasted tag URL or keyword tag. |
| `hashtags` | array | none | Alias of `keywords` (tags without the # symbol); merged with it. |
| `maxProfiles` | integer | `10` | Minimum `1`. Maximum profiles to scrape (profiles mode, or as review sources in reviews mode). Also caps the recent posts fetched per pasted tag, explore or directory URL. |
| `maxProfiles` (max) | integer | | Maximum `500`. |
| `maxReviewsPerProfile` | integer | `5` | Minimum `1`. Posts to fetch per profile, walked via load-more. |
| `maxReviewsPerProfile` (max) | integer | | Maximum `5000`. |
| `maxRepliesPerPost` | integer | `10` | Minimum `0`. Replies to keep per post when Include replies is on. |
| `maxRepliesPerPost` (max) | integer | | Maximum `200`. |
| `maxReviews` | integer | `1000` | Minimum `0`. Hard cap on total reviews returned in reviews mode; `0` (older saved tasks) is treated as `1000`. |
| `maxReviews` (max) | integer | | Maximum `100000`. |
| `maxPosts` | integer | `500` | Minimum `1`. Hard cap on total posts returned in keyword mode, split evenly across keywords. |
| `maxPosts` (max) | integer | | Maximum `100000`. |
| `resumeFromRunId` | string | (none) | Continue one specific interrupted run or dataset, without returning or billing records already collected there. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring; see "Resume and recurring updates" above. |
| `stateKey` | string | (none) | Name or share an incremental-mode monitoring campaign explicitly. |
| `emitUnchanged` | boolean | `false` | Also return `UNCHANGED` records (incremental mode only; bills extra rows). |
| `emitExpired` | boolean | `false` | Also return `EXPIRED` records (incremental mode only; bills extra rows). |
| `ignoreFieldsForChanges` | array | (none) | Field names left out of change detection (incremental mode only), e.g. `bio`; use `reviews.name` for fields inside a profile's `reviews` list. The fields are still returned. |
| `proxy` | object | Apify Proxy | Connection settings. |
| `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 item pages are created. |
| `maxNotifyListings` | integer | `50` | Minimum `1`. Cap on items written to each connector per run; does not affect the dataset. |
| `maxNotifyListings` (max) | integer | | Maximum `1000`. |

Caps: each profile returns at most `maxReviewsPerProfile` posts (default 5, maximum 5000), each post at most `maxRepliesPerPost` replies (default 10, maximum 200), and reviews mode at most `maxReviews` records in total (default 1000, maximum 100000). The per-profile default stays at 5 because every delivered post and reply is a billed record.

### Output Example

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

```json
{
  "recordType": "profile",
  "id": "990011223",
  "acct": "exampleuser",
  "username": "exampleuser",
  "displayName": "Example User",
  "bio": "Notes on open community projects and weekend hiking trips.",
  "followersCount": 4820,
  "followingCount": 312,
  "statusesCount": 1560,
  "url": "https://mastodon.example/@exampleuser",
  "avatar": "https://files.mastodon.example/accounts/avatars/000/990/011/223/original/avatar.png",
  "bot": false,
  "createdAt": "2019-05-02T00:00:00.000Z",
  "fields": [{ "name": "Homepage", "value": "example.org" }],
  "reviewCount": 2,
  "reviews": [
    {
      "recordType": "review",
      "id": "112233445566778899",
      "reviewType": "post",
      "contentText": "Sharing our monthly update on the project roadmap.",
      "createdAt": "2026-03-14T09:12:00.000Z",
      "repliesCount": 14,
      "reblogsCount": 37,
      "favouritesCount": 120,
      "url": "https://mastodon.example/@exampleuser/112233445566778899",
      "authorAcct": "exampleuser"
    },
    {
      "recordType": "review",
      "id": "112233445566778900",
      "reviewType": "reply",
      "contentText": "Thanks for the detailed writeup, this is helpful context.",
      "createdAt": "2026-03-14T09:40:00.000Z",
      "inReplyToId": "112233445566778899",
      "authorAcct": "sampleuser2"
    }
  ]
}
```

#### Output views

The dataset has three views in the Console. **Overview** shows profiles and posts together. **Posts and replies** gives one row per post or reply, with the reviews nested inside each profile row unwound into their own rows. **Changes (incremental)** lists rows reported by incremental mode with what changed.

### Plan Requirement

A paid Apify plan is required to run through Apify Proxy: free plans do not include proxy access. The actor starts on the standard Apify Proxy connection and tries a residential one only if that connection is refused (an ordinary timeout or error just retries on a fresh connection of the same kind) before giving up. It also rotates to a fresh exit when Mastodon rate-limits one (HTTP 429, about 300 requests per 5 minutes per address), so large or frequent runs may be slower. Residential is available under Connection if you prefer it, and you can supply your own proxy URLs instead.

When no page can be read the run says so and returns nothing, with one of these messages in the log and status:

- The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required. If you entered your own proxy URLs, check that their credentials are valid.
- No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Mastodon pages.
- Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search.

When the selected connection is unavailable but a backup one works, the log says the pages were served over the backup connection and some pages may still be unavailable.

### FAQ

#### How much does it cost?

You pay per run start and per record returned. In profiles mode, a surcharge applies once per profile whose replies were fetched and actually included in the pushed record; reviews and keyword mode never add this surcharge, since every reply there is already its own billed record. The **Pricing** tab shows the current rates. Use the max limits to cap the cost of any run.

#### Is it legal to scrape Mastodon?

This actor collects only information a Mastodon server already serves through its own public API to any visitor. You are responsible for how you use the result: usernames, bios and post content can identify real people, so follow the instance's terms, respect any account that has set itself to a locked or non-discoverable state, and get legal advice before any commercial redistribution.

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

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and reappeared records since the last run, and anything unchanged is not billed unless you turn Emit unchanged on.

#### What is the difference between Resume and Incremental mode?

Resume continues one specific interrupted run using a run or dataset ID you paste in; it is a one-time cleanup step. Incremental mode is for scheduling the same search again and again and getting a self-tracked diff each time, with no ID to paste. Use Resume after a run stops partway through; use Incremental mode for ongoing monitoring.

#### Why did my run finish with zero results instead of failing?

If a mode's source could not be reached at all, or nothing matched your input (an empty keyword list, URLs that do not resolve, and so on), the run finishes successfully with an explanatory status message and no dataset items, so you are never billed for a result you did not get. A run only fails outright when the input itself cannot be honored, such as a `resumeFromRunId` that does not resolve to a run or dataset you can access, or turning on Incremental mode together with `resumeFromRunId` while that search already has saved incremental state.

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

### 🔗 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/tiktok-scraper"><b>TikTok Profile, Hashtag, Search, Video &amp; Trending Scraper</b></a><br>Scrape TikTok without login. Extract profiles, bios, follower stats and videos; search by...</td><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></tr>
<tr><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><td>📱 <a href="https://apify.com/abotapi/truthsocial-com-scraper"><b>Truth Social Scraper</b></a><br>Scrape public Truth Social profiles, posts, media, metrics, author data, and complete...</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/reddit-scraper"><b>Reddit⚡ Lightning</b></a><br>Scrape Reddit posts, comments, and media from any subreddit or user profile - no login or...</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/mastodon-social-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

## `mode` (type: `string`):

What the actor returns. 'profiles' = one record per trending profile with its reviews nested inside. 'reviews' = one flat record per review (a post or a reply). 'keyword' = bulk posts for the keywords you list, one flat record per post, walked via load-more pagination (the high-volume option). 'urls' = scrape exactly the URLs listed in the URLs field (profiles output).

## `instanceUrl` (type: `string`):

Which Mastodon server to read the explore page from. Defaults to mastodon.social. Any public Mastodon instance works (for example https://mas.to or https://fosstodon.org).

## `profileSource` (type: `string`):

Where trending profiles come from. 'trending' = the authors of currently trending posts. 'directory' = the active public profile directory. 'both' = the two merged and deduped. Only applies when no URLs are provided.

## `onlyLocal` (type: `boolean`):

When using the directory source, return only accounts that are local to the chosen instance (skip federated accounts from other servers).

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

Mastodon URLs to scrape. Supported: profile (/@user, /@user/with_replies, /@user.rss, /users/user, /web/@user, other-instance /@user), post (/@user/ID, /@user/ID/embed, /users/user/statuses/ID, /web/statuses/ID), tag page (/tags/news), explore (/explore), profile directory (/directory), and the matching /api/v1 JSON URLs. Unsupported URLs are skipped with a warning. The prefilled list is one example per shape and runs as-is only when Output mode is 'Pasted URLs'; in other modes an unedited list is ignored, an edited list always applies.

## `accounts` (type: `array`):

Profile names to scrape, typed as people write them: Mastodon, @Mastodon, or user@other.instance. A leading @ is ignored. Names that do not resolve are skipped with a warning. Works in profiles and reviews modes.

## `keywords` (type: `array`):

Topics to bulk-scrape when mode is 'keyword'. Enter one word per entry, with or without a leading # (for example: news, art, climate). Each keyword is matched as a Mastodon tag and its public timeline is walked back via load-more pagination up to Max posts. Note: keywords match single tags, not free-text phrases (free-text search needs a token). A keyword with no matching posts is reported with a warning.

## `hashtags` (type: `array`):

Same as Keywords: tags to scrape without the # symbol (for example: opensource, linux). Merged with Keywords.

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

Fetch the reply thread under each post. Replies are added as reviews of type 'reply'. Turning this off makes runs faster and cheaper (one fewer request per post).

## `includeBoosts` (type: `boolean`):

Include posts the profile boosted (reblogged) from other accounts. Turn off to keep only the profile's own posts.

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

Keep only posts created on or after this date (YYYY-MM-DD or ISO datetime). Applied to the posts fetched for each profile, pasted tag URL, keyword tag or pasted post URL, so raise Max posts / Max reviews per profile to reach back further. Example: 2026-01-01.

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

Keep only posts created on or before this date (YYYY-MM-DD includes that whole day, or ISO datetime). Applied to the fetched window like Posts since. Example: 2026-12-31.

## `sortBy` (type: `string`):

Order of posts within each profile or tag: newest (default, platform order), oldest, most favourited, most reblogged or most replied. Sorting applies to the fetched window, not the whole history.

## `maxProfiles` (type: `integer`):

Maximum number of trending or directory profiles to scrape (profiles mode, or as review sources in reviews mode). Also caps how many recent posts are fetched from each pasted tag URL (/tags/NAME) and from each pasted explore or directory URL.

## `maxReviewsPerProfile` (type: `integer`):

How many of each profile's most recent posts to fetch. The profile timeline is walked back via load-more pagination, so you can pull deep history (up to 5000). Each post may also bring its replies (see Include replies).

## `maxRepliesPerPost` (type: `integer`):

Cap on how many replies to keep per post when Include replies is on.

## `maxReviews` (type: `integer`):

Hard cap on total reviews returned in reviews mode (maximum 100000). 0 (older saved tasks) is treated as the default of 1000.

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

Hard cap on total posts returned in keyword mode, split evenly across the keywords you list. Each keyword timeline is walked via load-more until this many posts are collected.

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

Proxy settings. The default connection works on every Apify plan, including the free tier. A residential connection is optional and only needed for very large or sustained runs.

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

Paste a previous run ID or dataset ID to continue an interrupted crawl without returning or charging for records already collected there. Use this after an interrupted run. For recurring monitoring of the same search (daily/weekly), use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns everything as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED records. Compared: every profile field (bio, display name, avatar, counts, locked, fields) and every post field (text, media, tags, counts on a post record). Excluded from the comparison (still returned in the output): the engagement counters of posts nested inside a profile record, the embedded author's follower/following/post counters on a post record, list ordering, and any field you name in "Ignore fields when detecting changes". Replies never produce EXPIRED, and EXPIRED is only produced for pasted profile/post URL runs, account-list runs and keyword runs that reached the natural end of every scope, never for explore (trending/directory) discovery and never when a pasted explore, directory or tag URL is among the sources. Turn on "Emit unchanged" or "Emit expired" only when you also want those records returned (and billed). State is kept separately for each mode/instance/search/filter setup (including Accounts, Include boosts and the since/until dates); 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 mode, instance, and every query/filter setting — different searches then never mix state with each other.

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

Off by default. Turn on to also return records 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 records that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked scope — not when a cap or Resume cut it short. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

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

Optional. Field names that must NOT make a record UPDATED or appear in changedFields, for example bio or followersCount. A bare name applies to the record and to each post inside a profile's reviews list; use reviews.name to limit it to posts inside the list. Ignored fields are still returned in the output; they are only left out of the comparison.

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

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

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

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

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

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "profiles",
  "instanceUrl": "https://mastodon.social",
  "profileSource": "both",
  "onlyLocal": false,
  "urls": [
    "https://mastodon.social/explore",
    "https://mastodon.social/@Mastodon",
    "https://mastodon.social/@Mastodon/with_replies",
    "https://mastodon.social/@Mastodon/117466985005297414",
    "https://mastodon.social/tags/news",
    "https://mastodon.social/users/Mastodon",
    "https://mastodon.social/web/@Mastodon",
    "https://mastodon.social/@Mastodon.rss",
    "https://fosstodon.org/@fosstodon",
    "https://mastodon.social/directory",
    "https://mastodon.social/api/v1/accounts/lookup?acct=Mastodon",
    "https://mastodon.social/@Mastodon/with_replies?max_id=1",
    "https://mastodon.social/@Mastodon/117466983307949301/embed",
    "https://mastodon.social/users/Mastodon/statuses/117411420916699261",
    "https://mastodon.social/web/statuses/117156549508722805",
    "https://mastodon.social/explore/posts",
    "https://mastodon.social/api/v1/timelines/tag/fediverse?limit=2",
    "https://mastodon.social/api/v1/trends/statuses?limit=2",
    "https://mastodon.social/api/v1/directory?limit=2",
    "https://mastodon.social/api/v1/statuses/117128801091131239/context"
  ],
  "accounts": [],
  "keywords": [
    "news"
  ],
  "includeReplies": true,
  "includeBoosts": true,
  "sortBy": "newest",
  "maxProfiles": 10,
  "maxReviewsPerProfile": 5,
  "maxRepliesPerPost": 10,
  "maxReviews": 1000,
  "maxPosts": 500,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `reviews` (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 = {
    "mode": "profiles",
    "instanceUrl": "https://mastodon.social",
    "profileSource": "both",
    "urls": [
        "https://mastodon.social/explore",
        "https://mastodon.social/@Mastodon",
        "https://mastodon.social/@Mastodon/with_replies",
        "https://mastodon.social/@Mastodon/117466985005297414",
        "https://mastodon.social/tags/news",
        "https://mastodon.social/users/Mastodon",
        "https://mastodon.social/web/@Mastodon",
        "https://mastodon.social/@Mastodon.rss",
        "https://fosstodon.org/@fosstodon",
        "https://mastodon.social/directory",
        "https://mastodon.social/api/v1/accounts/lookup?acct=Mastodon",
        "https://mastodon.social/@Mastodon/with_replies?max_id=1",
        "https://mastodon.social/@Mastodon/117466983307949301/embed",
        "https://mastodon.social/users/Mastodon/statuses/117411420916699261",
        "https://mastodon.social/web/statuses/117156549508722805",
        "https://mastodon.social/explore/posts",
        "https://mastodon.social/api/v1/timelines/tag/fediverse?limit=2",
        "https://mastodon.social/api/v1/trends/statuses?limit=2",
        "https://mastodon.social/api/v1/directory?limit=2",
        "https://mastodon.social/api/v1/statuses/117128801091131239/context"
    ],
    "accounts": [],
    "keywords": [
        "news"
    ],
    "maxProfiles": 10,
    "maxReviewsPerProfile": 5,
    "maxRepliesPerPost": 10,
    "maxPosts": 500,
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/mastodon-social-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 = {
    "mode": "profiles",
    "instanceUrl": "https://mastodon.social",
    "profileSource": "both",
    "urls": [
        "https://mastodon.social/explore",
        "https://mastodon.social/@Mastodon",
        "https://mastodon.social/@Mastodon/with_replies",
        "https://mastodon.social/@Mastodon/117466985005297414",
        "https://mastodon.social/tags/news",
        "https://mastodon.social/users/Mastodon",
        "https://mastodon.social/web/@Mastodon",
        "https://mastodon.social/@Mastodon.rss",
        "https://fosstodon.org/@fosstodon",
        "https://mastodon.social/directory",
        "https://mastodon.social/api/v1/accounts/lookup?acct=Mastodon",
        "https://mastodon.social/@Mastodon/with_replies?max_id=1",
        "https://mastodon.social/@Mastodon/117466983307949301/embed",
        "https://mastodon.social/users/Mastodon/statuses/117411420916699261",
        "https://mastodon.social/web/statuses/117156549508722805",
        "https://mastodon.social/explore/posts",
        "https://mastodon.social/api/v1/timelines/tag/fediverse?limit=2",
        "https://mastodon.social/api/v1/trends/statuses?limit=2",
        "https://mastodon.social/api/v1/directory?limit=2",
        "https://mastodon.social/api/v1/statuses/117128801091131239/context",
    ],
    "accounts": [],
    "keywords": ["news"],
    "maxProfiles": 10,
    "maxReviewsPerProfile": 5,
    "maxRepliesPerPost": 10,
    "maxPosts": 500,
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/mastodon-social-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 '{
  "mode": "profiles",
  "instanceUrl": "https://mastodon.social",
  "profileSource": "both",
  "urls": [
    "https://mastodon.social/explore",
    "https://mastodon.social/@Mastodon",
    "https://mastodon.social/@Mastodon/with_replies",
    "https://mastodon.social/@Mastodon/117466985005297414",
    "https://mastodon.social/tags/news",
    "https://mastodon.social/users/Mastodon",
    "https://mastodon.social/web/@Mastodon",
    "https://mastodon.social/@Mastodon.rss",
    "https://fosstodon.org/@fosstodon",
    "https://mastodon.social/directory",
    "https://mastodon.social/api/v1/accounts/lookup?acct=Mastodon",
    "https://mastodon.social/@Mastodon/with_replies?max_id=1",
    "https://mastodon.social/@Mastodon/117466983307949301/embed",
    "https://mastodon.social/users/Mastodon/statuses/117411420916699261",
    "https://mastodon.social/web/statuses/117156549508722805",
    "https://mastodon.social/explore/posts",
    "https://mastodon.social/api/v1/timelines/tag/fediverse?limit=2",
    "https://mastodon.social/api/v1/trends/statuses?limit=2",
    "https://mastodon.social/api/v1/directory?limit=2",
    "https://mastodon.social/api/v1/statuses/117128801091131239/context"
  ],
  "accounts": [],
  "keywords": [
    "news"
  ],
  "maxProfiles": 10,
  "maxReviewsPerProfile": 5,
  "maxRepliesPerPost": 10,
  "maxPosts": 500,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/mastodon-social-scraper --silent --output-dataset

```

## MCP server setup

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