# Mastodon $1💰 URL, Trend & Profile Scraper (`abotapi/mastodon-social-scraper`) Actor

From $1/1K. Scrape trending Mastodon profiles and related posts from any Mastodon instance. Returns rich profile data, follower counts, bios, avatars, fields, and thread replies. Supports nested profile reviews or flat review output.

- **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
- **Stats:** 2 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?

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

## Mastodon Explore Scraper

Pull the trending profiles and their content from any Mastodon instance's explore page, straight from the public Mastodon API. You get rich profile records (bio, follower counts, fields, avatar) and their reviews, where a review is a post or a reply in its thread. Choose between profiles with reviews nested inside, a flat stream of reviews, or bulk posts by keyword. Defaults to mastodon.social and works on any Mastodon server.

### Why this scraper

- Three output modes: trending profiles with nested reviews, reviews only (one flat record each), or keyword posts in bulk.
- Keyword mode harvests posts by topic at volume: each keyword timeline is walked back via load-more pagination up to your post cap (tens of thousands of posts).
- Reviews cover both a profile's own posts and the replies underneath them, so you capture the full conversation.
- Deep history: pull up to thousands of a profile's past posts, walked automatically via load-more pagination.
- Reads the public Mastodon REST API directly: fast, stable JSON, no fragile page parsing.
- Trending profiles merge two signals: authors of currently trending posts and the active public directory.
- URL mode: paste profile or post links to scrape exactly what you want.
- Works on any instance (mastodon.social, mas.to, fosstodon.org, your own server).
- Every record keeps the full upstream account object, so no field is ever dropped.

### Data you get

Profile record (profiles mode):

| Field | Example |
| --- | --- |
| recordType | profile |
| id | 13179 |
| acct | Mastodon |
| username | Mastodon |
| displayName | Mastodon |
| bio | Free, open-source decentralized social media. Not for sale. |
| followersCount | 853000 |
| followingCount | 4 |
| statusesCount | 700 |
| url | https://mastodon.social/@Mastodon |
| avatar | https://files.mastodon.social/accounts/avatars/000/013/179/original/...png |
| bot | false |
| createdAt | 2016-11-23T00:00:00.000Z |
| fields | \[{"name": "Homepage", "value": "joinmastodon.org"}] |
| reviewCount | 5 |
| reviews | \[ ... array of review records ... ] |

Review record (reviews mode, keyword mode, or nested under a profile):

| Field | Example |
| --- | --- |
| recordType | review |
| id | 109876543210987654 |
| reviewType | post (or reply) |
| isReblog | false |
| rebloggedByAcct | null (set to the booster's handle when isReblog is true) |
| sourceKeyword | news (keyword mode only; null otherwise) |
| contentText | We just shipped a new version with quote posts. |
| createdAt | 2026-05-20T14:03:11.000Z |
| language | en |
| url | https://mastodon.social/@Mastodon/109876543210987654 |
| repliesCount | 42 |
| reblogsCount | 318 |
| favouritesCount | 901 |
| inReplyToId | null |
| tags | \["mastodon", "fediverse"] |
| authorAcct | Mastodon |
| authorDisplayName | Mastodon |
| account | { ... full upstream account object ... } |

### How to use

Trending profiles with their reviews (default):

```json
{
  "mode": "profiles",
  "instanceUrl": "https://mastodon.social/",
  "profileSource": "both",
  "includeReplies": true,
  "maxProfiles": 10,
  "maxReviewsPerProfile": 5,
  "maxRepliesPerPost": 10
}
```

Reviews only, capped at 200, 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", "climate"],
  "includeReplies": false,
  "maxPosts": 5000
}
```

Deep history of one profile (thousands of past posts):

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

Active local directory profiles only:

```json
{
  "mode": "profiles",
  "profileSource": "directory",
  "onlyLocal": true,
  "maxProfiles": 25
}
```

URL mode (specific profiles and posts):

```json
{
  "mode": "reviews",
  "urls": [
    "https://mastodon.social/@Mastodon",
    "https://mastodon.social/@Gargron",
    "https://mastodon.social/@Mastodon/109876543210987654"
  ],
  "includeReplies": true
}
```

### 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 |
| profileSource | string | both | trending, directory, or both (used when no URLs) |
| onlyLocal | boolean | false | Directory: local accounts only |
| urls | array | \[] | Profile or post URLs; when set, overrides explore/profiles/reviews/keyword |
| keywords | array | \[news] | Keywords to bulk-scrape in keyword mode (matched as tags, with or without #) |
| includeReplies | boolean | true | Fetch the reply thread under each post |
| maxProfiles | integer | 10 | Max profiles to scrape (up to 500) |
| maxReviewsPerProfile | integer | 5 | Posts to fetch per profile, walked via load-more (up to 5000) |
| maxRepliesPerPost | integer | 10 | Replies to keep per post |
| maxReviews | integer | 0 | Reviews-mode total cap (0 = unlimited) |
| maxPosts | integer | 500 | Keyword-mode total cap, split across keywords (up to 100000) |
| resumeFromRunId | string | (none) | Continue one specific interrupted run/dataset without re-collecting or re-charging |
| incrementalMode | boolean | false | Turn on for recurring monitoring; see "Resume & recurring updates" above |
| stateKey | string | (auto) | 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) |
| proxy | object | Apify datacenter | Proxy configuration |

### Resume & recurring updates

Two different things, both optional:

- **Resume from a previous run** (`resumeFromRunId`): paste a previous run ID or dataset ID to continue an interrupted crawl without returning or re-charging for records already collected there.
- **Incremental mode** (`incrementalMode`): turn this on for daily/weekly recurring monitoring of the *same* search. The actor remembers its own baseline (keyed by mode + instance + your query/filter settings, excluding the `max*` depth caps) in a key-value store — no run/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 (and billed — they're extra dataset items).

When `incrementalMode` is on, every pushed record gains four fields:

| Field | Meaning |
| --- | --- |
| changeType | `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED` |
| changedFields | Which fields differed from the last run (empty for NEW/UNCHANGED/REAPPEARED/EXPIRED) |
| firstSeenAt | When this record was first observed by this monitoring campaign |
| lastSeenAt | When this record was last observed |

`EXPIRED` rows (records that were present before but are no longer found) are only produced once a run has fully scanned its tracked scope — never when a cap, a failed page, an instance hiccup, or Resume cut a run short, since a partial scan can't tell "gone" apart from "not reached yet".

**What counts as a real change vs. noise:** `followersCount`/`followingCount`/`statusesCount` on a profile, and `favouritesCount`/`reblogsCount`/`repliesCount` on a top-level review/keyword-mode record, are genuinely volatile but treated as **real data** — a profile gaining a follower, or a trending post picking up favourites, is exactly what a recurring run should report. The same engagement counters on a review *nested inside a profile record* (profiles mode) are excluded from change detection there only — a nested post's favourite ticking would otherwise flag the whole profile `UPDATED` on every run and drown out real profile-level changes (bio, avatar, display name). A reply/post-listing fetch that fails outright never corrupts the baseline: the affected fields carry forward from the last successful scrape instead of being read as "this profile now has zero posts".

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

### Output example

```json
{
  "recordType": "profile",
  "id": "13179",
  "acct": "Mastodon",
  "username": "Mastodon",
  "displayName": "Mastodon",
  "bio": "Free, open-source decentralized social media. Not for sale.",
  "followersCount": 853000,
  "followingCount": 4,
  "statusesCount": 700,
  "url": "https://mastodon.social/@Mastodon",
  "bot": false,
  "createdAt": "2016-11-23T00:00:00.000Z",
  "fields": [{ "name": "Homepage", "value": "joinmastodon.org" }],
  "reviewCount": 2,
  "reviews": [
    {
      "recordType": "review",
      "id": "109876543210987654",
      "reviewType": "post",
      "contentText": "We just shipped a new version with quote posts.",
      "createdAt": "2026-05-20T14:03:11.000Z",
      "repliesCount": 42,
      "reblogsCount": 318,
      "favouritesCount": 901,
      "url": "https://mastodon.social/@Mastodon/109876543210987654",
      "authorAcct": "Mastodon"
    },
    {
      "recordType": "review",
      "id": "109876543299990000",
      "reviewType": "reply",
      "contentText": "Congrats, this is huge for the fediverse!",
      "createdAt": "2026-05-20T14:09:55.000Z",
      "inReplyToId": "109876543210987654",
      "authorAcct": "someuser"
    }
  ]
}
```

### Plan requirement

Runs on any Apify plan, including the free tier. The default connection works on every plan. A residential proxy connection is optional and only worth turning on for very large or sustained runs.

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

## `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`):

Optional. Paste Mastodon profile URLs (https://mastodon.social/@username) or post URLs (https://mastodon.social/@username/123456789). When provided, the explore search is skipped and the actor scrapes exactly these instead. Leave empty to use profiles, reviews, or keyword mode. Multiple URLs supported.

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

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

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

Maximum number of trending profiles to scrape (profiles mode, or as review sources in reviews mode).

## `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. 0 means unlimited (bounded by the profile and per-post limits above).

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

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

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

## `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": [],
  "keywords": [
    "news"
  ],
  "includeReplies": true,
  "maxProfiles": 10,
  "maxReviewsPerProfile": 5,
  "maxRepliesPerPost": 10,
  "maxReviews": 0,
  "maxPosts": 500,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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": [],
    "keywords": [
        "news"
    ],
    "maxProfiles": 10,
    "maxReviewsPerProfile": 5,
    "maxRepliesPerPost": 10,
    "maxPosts": 500,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// 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": [],
    "keywords": ["news"],
    "maxProfiles": 10,
    "maxReviewsPerProfile": 5,
    "maxRepliesPerPost": 10,
    "maxPosts": 500,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# 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": [],
  "keywords": [
    "news"
  ],
  "maxProfiles": 10,
  "maxReviewsPerProfile": 5,
  "maxRepliesPerPost": 10,
  "maxPosts": 500,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
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/9AcNNlskL3tOKJTbi/openapi.json
