# Mastodon Posts by Hashtag, Account, Thread & Trends (no login) (`yadroo/mastodon-public-posts`) Actor

Public Mastodon data as rows: posts of one or more hashtags with boolean tag combinations, an account's public posts, the whole reply tree of a post, trending tags and news links, hashtag discovery with 7-day usage, and profile counts. Authors from any federated server. Keyless API, UTC dates.

- **URL**: https://apify.com/yadroo/mastodon-public-posts.md
- **Developed by:** [Samat Makatov](https://apify.com/yadroo) (community)
- **Categories:** Social media, News, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 mastodon data rows

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

Public Mastodon data as clean rows, straight from the REST API of the **mastodon.social** server: every post under a hashtag (with boolean tag combinations), the public posts of the accounts you name, the whole reply tree under one post, what the network trends on right now (hashtags, shared news links, popular posts), which hashtags exist for a keyword and how much they are used, and follower/post counts for a watch list. Because that server federates, authors from thousands of other fediverse servers show up too, and every row says which server its author is on. No API key, no token, no cookie, no browser — and all timestamps are UTC ISO-8601.

> **Need it tailored to your workflow?** Missing a field, a filter, another source or a different output format? [Open an issue](https://apify.com/yadroo/mastodon-public-posts/issues) and describe what you need — we read every request and adapt the actor.

### Use cases

- **Replace a closed forum feed**: follow `#opensource`, `#selfhosted` or your product name on an open network with `sinceHours` + `onlyNew` on a schedule, and get only what was posted since the last run.
- **Brand and press monitoring**: export the public posts of an organisation's accounts (`accountPosts`) with favourites, boosts and replies, and archive announcements with their links and media.
- **Community management**: turn one announcement into its full discussion (`threadReplies`) — every reply the server knows, with its depth, author and server, so you can read or summarise a reaction in one pass.
- **Newsroom and PR monitoring**: `trending` + `links` gives the articles the network is sharing right now with publisher, publication date and how many accounts shared them today and over the week.
- **Dashboard planning**: `hashtagSearch` shows which spelling of a topic is alive (`climate` vs `climatechange` vs `climatecrisis`) with seven days of usage, before you commit a monitoring run to the wrong tag.
- **Research and scouting**: `profiles` keeps follower, following and post counts of a list of local and remote handles fresh; `authorInstances` lets you study one community inside a worldwide hashtag.
- **Agents and LLM pipelines**: public social text and trend lists without any credential — call the actor over the Apify API or MCP and narrow the columns with `fields`.

### Input

Every field is optional; each mode uses the fields of its own section. Example values in the editor are **prefills** (you can change or clear them), defaults only switch behaviour.

| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
| `mode` | string | `hashtagPosts` | `hashtagPosts`, `accountPosts`, `threadReplies`, `trending`, `hashtagSearch`, `profiles` — see [Modes](#modes) |
| `hashtags` | string\[] | prefill `opensource`, `linux` | Hashtags to read in `hashtagPosts`, with or without `#`, case ignored. The row limit is split evenly over them |
| `alsoTagged` | string\[] | – | `hashtagPosts`: keep posts carrying the main tag **or** any of these (source parameter `any`) |
| `mustAlsoBeTagged` | string\[] | – | `hashtagPosts`: keep only posts carrying the main tag **and** all of these (`all`) |
| `excludeTagged` | string\[] | – | `hashtagPosts`: drop posts that also carry one of these (`none`) |
| `localOnly` | boolean | `false` | `hashtagPosts`: keep only posts written on mastodon.social itself |
| `accounts` | string\[] | prefill `Mastodon`, `MastodonEngineering` | `accountPosts` and `profiles`. Forms: `name`, `name@server`, profile link — see [Handles](#handles) |
| `excludeReplies` | boolean | `true` | `accountPosts`: leave out the replies the account wrote |
| `includeReblogs` | boolean | `false` | `accountPosts`: also keep posts the account boosted (`isReblog: true`, booster in `rebloggedBy`) |
| `postUrls` | string\[] | prefill one post link | `threadReplies`: mastodon.social post links or bare numeric ids — see [Post links](#post-links) |
| `includeAncestors` | boolean | `true` | `threadReplies`: also write the posts above the given one (negative `replyDepth`) |
| `trendType` | string | `tags` | `trending`: `tags`, `links` or `posts` — see [Trend types](#trend-types) |
| `searchQuery` | string | prefill `climate` | `hashtagSearch`: one word; matches hashtag **names**, not post text |
| `languages` | string\[] | all | Keep posts whose declared language is one of these 20 codes — see [Languages](#languages) |
| `minFavourites` | number | – | 0–1 000 000. Keep posts with at least this many favourites at fetch time |
| `minReblogs` | number | – | 0–1 000 000. Same floor for boosts |
| `excludeSensitive` | boolean | `false` | Drop posts flagged sensitive or behind a content warning |
| `onlyWithMedia` | boolean | `false` | Keep only posts with an image, video or audio attachment |
| `authorInstances` | string\[] | all | Keep posts whose author lives on one of these servers, e.g. `fosstodon.org` |
| `sinceHours` | number | – | 1–8760. Rolling window in UTC hours. A timeline stops paging once it passes the window; the trending-posts list is ranked by popularity, so older posts there are dropped one by one instead |
| `onlyNew` | boolean | `false` | Write only posts that earlier runs of the same actor or task did not write |
| `maxItems` | number | `50` | 1–1000. Hard cap on rows, split over the hashtags or accounts you named. The run also stops at your **Maximum cost per run** (see [Limits](#limits--faq)) |
| `fields` | string\[] | all | Keep only these columns, in this order. `rowType`, `sourceMode` and the row's id column (`statusId`+`found`, `tag`, `url` or `handle`+`found`) are always kept. Column names are free text: case does not matter and one near miss such as `favoriteCount` is corrected, both noted in the status message; a name no row has is reported and ignored, and a list in which no name exists fails the run with the valid names |

Filters are applied after the source answers, except `localOnly`, `onlyWithMedia` and the three tag combinations, which the source itself applies before paging. `languages`, `minFavourites`, `minReblogs`, `excludeSensitive`, `onlyWithMedia`, `authorInstances`, `sinceHours` and `onlyNew` only affect post rows; `trending` with `tags` or `links`, `hashtagSearch` and `profiles` ignore them and say so in the status message.

### Reference

#### Modes

| `mode` | One row per | Needs | Writes |
|---|---|---|---|
| `hashtagPosts` | public post carrying one of your hashtags, newest first | `hashtags` | post rows, `matchedHashtag` filled |
| `accountPosts` | public post of the accounts you name | `accounts` | post rows |
| `threadReplies` | post in the discussion under a post | `postUrls` | post rows with `replyDepth`, `threadRootStatusId` |
| `trending` | trending hashtag, shared news link or popular post | `trendType` | tag, link or post rows with `trendRank` |
| `hashtagSearch` | hashtag whose name matches your word | `searchQuery` | tag rows with seven days of usage |
| `profiles` | account, with its public counts | `accounts` | profile rows |

#### Trend types

| `trendType` | Source list | Row type | Rows per run |
|---|---|---|---|
| `tags` | hashtags the server currently promotes | `tag` | 20 per request, paged |
| `links` | articles being shared right now | `link` | 20 per request, paged |
| `posts` | individual posts the server promotes | `post` | 40 per request, paged |

#### Handles

`accounts` accepts three shapes, mixed freely in one list:

| You write | Means |
|---|---|
| `Mastodon` or `@Mastodon` | the account on mastodon.social |
| `pluralistic@mamot.fr` | an account on any other fediverse server |
| `https://fosstodon.org/@kev` | the same, from a profile link you pasted |

Case does not matter. An account the server has never federated with cannot be resolved and yields one row with `found: false` and the reason in `notFoundReason`.

#### Post links

`postUrls` accepts mastodon.social links and bare ids: `https://mastodon.social/@Mastodon/117156549508722805`, `https://mastodon.social/@hackernews@robot.villas/117367636517428091`, `https://mastodon.social/users/Mastodon/statuses/117156549508722805` or `117156549508722805`.

Post ids are numbered **per server**, so a link on another server (`https://mastodon.online/@someone/1171565933…`) is refused with a message instead of a wrong answer. Every post row of every mode carries the mastodon.social link of the same post in `localUrl` — take that value and `threadReplies` will accept it.

#### Languages

The `language` column is what the posting app declared, not a guess of ours. Codes offered by the filter: `en` English, `de` German, `fr` French, `es` Spanish, `pt` Portuguese, `it` Italian, `nl` Dutch, `ja` Japanese, `zh` Chinese, `ko` Korean, `ru` Russian, `uk` Ukrainian, `pl` Polish, `cs` Czech, `sv` Swedish, `fi` Finnish, `nb` Norwegian, `da` Danish, `tr` Turkish, `ca` Catalan. Send one of these 20 two-letter codes exactly: the platform checks the list before the run starts, so a language name (`German`), a three-letter code (`deu`) or a code outside the list is refused with the list of allowed values. Regional variants count as their language (`en-GB` is `en`), and `nb` (Norwegian) also keeps posts declared `no` (Norwegian) or `nn` (Nynorsk); the `language` column still shows what the post declared. Many posts from other fediverse software declare no language at all — those are dropped while this filter is set.

#### Row types

`rowType` says what a row is: `post`, `tag` (from `trending`+`tags` and `hashtagSearch`), `link` (from `trending`+`links`) or `profile`. One run writes one row type, so a dataset never mixes them.

### Examples

**Follow two topics for the last two days, without sensitive posts**

```json
{ "mode": "hashtagPosts", "hashtags": ["opensource", "linux"], "sinceHours": 48, "excludeSensitive": true, "maxItems": 20 }
```

**One cheap request for a niche: Linux AND open source, but no AI crossposts**

```json
{ "mode": "hashtagPosts", "hashtags": ["linux"], "mustAlsoBeTagged": ["opensource"], "excludeTagged": ["ai"], "maxItems": 20 }
```

**Export what two organisation accounts published**

```json
{ "mode": "accountPosts", "accounts": ["Mastodon", "MastodonEngineering"], "excludeReplies": true, "includeReblogs": false, "maxItems": 20 }
```

**Read the whole discussion under one announcement**

```json
{ "mode": "threadReplies", "postUrls": ["https://mastodon.social/@Mastodon/117156549508722805"], "includeAncestors": true, "maxItems": 25 }
```

**What is trending right now, and which articles are being shared**

```json
{ "mode": "trending", "trendType": "tags", "maxItems": 20 }
```

```json
{ "mode": "trending", "trendType": "links", "maxItems": 20 }
```

**Find the hashtag spelling that is actually used, then watch one community inside it**

```json
{ "mode": "hashtagSearch", "searchQuery": "climate", "maxItems": 20 }
```

```json
{ "mode": "hashtagPosts", "hashtags": ["climatechange"], "authorInstances": ["fosstodon.org", "mastodon.social"], "languages": ["en"], "maxItems": 20 }
```

**Keep a watch list of accounts up to date**

```json
{ "mode": "profiles", "accounts": ["Mastodon", "MastodonEngineering", "pluralistic@mamot.fr"], "maxItems": 10 }
```

### Output

One real row of a cloud run (`{"mode": "hashtagPosts", "hashtags": ["opensource", "linux"], "sinceHours": 48, "excludeSensitive": true, "maxItems": 20}`, run `9A6iUYc6Nh7L8lnMA`):

```json
{
  "rowType": "post",
  "sourceMode": "hashtagPosts",
  "statusId": "117367721941465389",
  "url": "https://mastodon.social/@joelindien/117367721941465389",
  "localUrl": "https://mastodon.social/@joelindien/117367721941465389",
  "createdAt": "2026-10-01T21:15:11.852Z",
  "editedAt": null,
  "language": "fr",
  "visibility": "public",
  "sensitive": false,
  "contentWarning": null,
  "text": "🐧 Actu logiciel libre du 01/10/2026\n• Fedora 45 Beta Performance: The Satisfyingly Boring Benchmarks\n[Phoronix] https://www.phoronix.com/review/fedora-45-beta-benchmarks\n#LogicielLibre #OpenSource #Libre",
  "authorHandle": "joelindien",
  "authorFullHandle": "joelindien@mastodon.social",
  "authorDisplayName": null,
  "authorInstance": "mastodon.social",
  "authorUrl": "https://mastodon.social/@joelindien",
  "authorFollowers": 14,
  "authorPosts": 2927,
  "authorIsBot": false,
  "replyCount": 0,
  "reblogCount": 0,
  "favouriteCount": 0,
  "quoteCount": 0,
  "hashtags": [
    "logiciellibre",
    "opensource",
    "libre"
  ],
  "matchedHashtag": "opensource",
  "mentions": [],
  "linkUrls": [
    "https://www.phoronix.com/review/fedora-45-beta-benchmarks"
  ],
  "cardUrl": "https://www.phoronix.com/review/fedora-45-beta-benchmarks",
  "cardTitle": "Fedora 45 Beta Performance: The Satisfyingly Boring Benchmarks",
  "cardPublisher": null,
  "mediaCount": 0,
  "mediaTypes": [],
  "mediaUrls": [],
  "isReblog": false,
  "rebloggedBy": null,
  "inReplyToStatusId": null,
  "replyDepth": null,
  "threadRootStatusId": null,
  "threadRootUrl": null,
  "applicationName": "bot news",
  "sourceRef": "opensource",
  "found": true,
  "notFoundReason": null,
  "fetchedAt": "2026-10-01T21:19:14.212Z"
}
```

#### Post row (`rowType: "post"`)

| Field | Type | Meaning |
|---|---|---|
| `rowType` | string | `post` |
| `sourceMode` | string | The mode that produced the row |
| `statusId` | string | The post id on mastodon.social — feed it back into `threadReplies` |
| `url` | string | The post as its own server links it (the canonical link) |
| `localUrl` | string | The same post on mastodon.social; always usable in `postUrls` |
| `createdAt` | string | When the post was written, ISO 8601 UTC |
| `editedAt` | string | When it was last edited, or `null` |
| `language` | string | Language declared by the posting app (often `null` for other software) |
| `visibility` | string | `public` or `unlisted` — what the public API serves |
| `sensitive` | boolean | Flagged sensitive by its author, or carrying a content warning |
| `contentWarning` | string | The warning text, or `null` |
| `text` | string | Post text as a reader sees it: markup removed, links written out in full |
| `authorHandle` | string | Author name without the server, lowercased |
| `authorFullHandle` | string | `name@server` for every author, so rows can be compared |
| `authorDisplayName` | string | Name the author shows |
| `authorInstance` | string | Server the author lives on, e.g. `fosstodon.org` |
| `authorUrl` | string | Author's profile link |
| `authorFollowers`, `authorPosts` | number | Public counts of the author at fetch time |
| `authorIsBot` | boolean | The account marks itself as automated |
| `replyCount`, `reblogCount`, `favouriteCount`, `quoteCount` | number | Engagement at `fetchedAt` |
| `hashtags` | string\[] | Every hashtag of the post, lowercased |
| `matchedHashtag` | string | The hashtag of your input that produced this row (`hashtagPosts`) |
| `mentions` | string\[] | Handles mentioned in the post |
| `linkUrls` | string\[] | URLs the post links to |
| `cardUrl`, `cardTitle`, `cardPublisher` | string | Link preview the server built, when there is one |
| `mediaCount` | number | Number of attachments |
| `mediaTypes` | string\[] | `image`, `video`, `gifv`, `audio` |
| `mediaUrls` | string\[] | File links of the attachments (linked, never downloaded) |
| `isReblog` | boolean | The row came from a boost (`accountPosts` with `includeReblogs`) |
| `rebloggedBy` | string | Who boosted it |
| `inReplyToStatusId` | string | The post this one replies to, or `null` |
| `replyDepth` | number | `threadReplies`: `0` for the post you asked for, `1`, `2`… for replies, negative above it |
| `threadRootStatusId`, `threadRootUrl` | string | `threadReplies`: the post the thread was read from |
| `applicationName` | string | App that published the post, when the server knows it |
| `sourceRef` | string | The exact input value this row came from |
| `found` | boolean | `false` on the one row that explains an unknown handle, id or opt-out |
| `notFoundReason` | string | Why that input gave no data |
| `fetchedAt` | string | Fetch time, ISO 8601 UTC — engagement counts are a snapshot of this moment |

#### Tag row (`rowType: "tag"`)

| Field | Type | Meaning |
|---|---|---|
| `tag` | string | Hashtag name as the server spells it |
| `url` | string | Tag page on mastodon.social |
| `usesToday`, `accountsToday` | number | Uses and distinct accounts on the newest day the server reports |
| `uses7d`, `accounts7d` | number | Sum over the seven days the server sends |
| `history` | object\[] | `{ day (UTC date), accounts, uses }`, newest first |
| `trendRank` | number | Position in the list the source returned, from 1 |
| `sourceRef` | string | `trending-tags`, or your `searchQuery` |

#### Link row (`rowType: "link"`)

| Field | Type | Meaning |
|---|---|---|
| `title`, `description` | string | Article title and teaser as the server read them |
| `url` | string | The article itself |
| `publisherName`, `authorName` | string | Publisher and author, when the page declares them |
| `publishedAt` | string | Publication date, ISO 8601 UTC |
| `language` | string | Language of the article |
| `imageUrl` | string | Preview image the server cached |
| `usesToday`, `accountsToday`, `uses7d`, `accounts7d` | number | How often and by how many accounts it was shared |
| `trendRank` | number | Position in the trending list |

#### Profile row (`rowType: "profile"`)

| Field | Type | Meaning |
|---|---|---|
| `handle`, `fullHandle` | string | `mastodon` and `mastodon@mastodon.social` |
| `displayName` | string | Name the account shows |
| `instance` | string | Server the account lives on |
| `bio` | string | Profile text, markup removed |
| `followersCount`, `followingCount`, `postsCount` | number | Public counts (the **lists** are never collected) |
| `createdAt` | string | Join date, ISO 8601 UTC |
| `lastPostAt` | string | Date of the last post the server knows (UTC date) |
| `isBot`, `isLocked`, `isDiscoverable` | boolean | Flags the account set itself |
| `profileFields` | object\[] | `{ name, value, verified }` of the links next to the bio |
| `avatarUrl`, `url` | string | Avatar file and profile link |
| `found`, `notFoundReason` | boolean, string | `false` for an unknown handle or an account that opted out |

Dataset views: **Posts** (author, server, text, posted, language, favourites, boosts, replies, hashtags, matched tag, link), **Trending tags and links** (kind, tag, article, publisher, daily and weekly counts, link), **Trending and searched hashtags** (rank, hashtag, uses and accounts today and over seven days, list or search word, tag page), **Trending news links** (rank, article, publisher, published, language, shares today and over seven days, link), **Profiles** (handle, name, server, followers, following, posts, joined, last post, link). The dataset schema describes every column with a title, a description and an example, for agents and MCP clients.

Every run also writes a `SUMMARY` record into the default key-value store (linked from the run's output as `summary`): the mode and the filters it ran with, the number of requests, the rate-limit budget the source reported and whether its limit stopped the run, rows read and written, rows per hashtag/account/thread, inputs that were not served, inputs without rows, a counter per filter that dropped something, and why the run stopped early when it did (your spending limit, the run timeout).

### Use it from code / agents

```bash
curl -X POST "https://api.apify.com/v2/acts/yadroo~mastodon-public-posts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"hashtagPosts","hashtags":["opensource"],"sinceHours":24,"maxItems":20,"fields":["authorFullHandle","text","favouriteCount","createdAt","url"]}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/mastodon-public-posts').call({ mode: 'trending', trendType: 'links', maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) console.log(row.publisherName, row.usesToday, row.url);
```

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
## 1) read a hashtag, 2) take the localUrl of the loudest post, 3) read its thread
posts = client.actor("yadroo/mastodon-public-posts").call(
    run_input={"mode": "hashtagPosts", "hashtags": ["opensource"], "minFavourites": 5, "maxItems": 20}
)
rows = client.dataset(posts["defaultDatasetId"]).list_items().items
loudest = max(rows, key=lambda r: r.get("favouriteCount") or 0)
thread = client.actor("yadroo/mastodon-public-posts").call(
    run_input={"mode": "threadReplies", "postUrls": [loudest["localUrl"]], "maxItems": 25}
)
```

Chaining works the same way in the other direction: a `trending` + `tags` run gives you `tag` values to put straight into `hashtagPosts`. For schedules, set `sinceHours` to the interval and `onlyNew: true` — each run then holds only what is new, and an empty run means nothing was posted. Narrow rows with `fields` when an LLM reads them.

MCP: add `https://mcp.apify.com` to Claude / Cursor / any MCP client and call the `yadroo/mastodon-public-posts` tool with the same JSON input.

### Pricing

Pay per event: **$0.001 per run start + $0.0015 per dataset row**. The start event is charged on every run, including runs that write no row.

| Run | Rows | Cost |
|---|---|---|
| Trending hashtags, one request | 20 | $0.001 + 20 × $0.0015 = **$0.031** |
| Hashtag monitoring, last 24 h | 50 | $0.001 + 50 × $0.0015 = **$0.076** |
| Thread of a busy announcement | 200 | $0.001 + 200 × $0.0015 = **$0.301** |

Platform usage is included: you pay only these two events, never compute or proxy on top. A 50-row run needs 2–3 HTTP requests and finishes in well under a minute at 256 MB. Bronze −10 %, Silver −20 %, Gold and above −30 % on the row price; the start event is the same on every plan.

### Limits & FAQ

- **No full-text post search.** The source serves post search only to callers with a login: `type=statuses` answers empty arrays without one. Topics are therefore reached by hashtag, by account or through the trending lists — and `hashtagSearch` exists to find the right hashtag first. This is a limit of the source, not something a token of ours could widen.
- **No "everything" mode.** The anonymous public timeline of the server answers HTTP 422, so there is no firehose of all posts. Every run names hashtags, accounts, a post or a trend list.
- **One server's federated view.** Data comes from mastodon.social. Posts and accounts of other fediverse servers appear through it — which is most of the network for popular tags — but a post only exists here if this server has seen it, and `authorInstance` tells you where each author actually lives. Replies that live only on a server this one does not follow are not part of a thread.
- **Trends are that server's view.** The trending lists are computed by mastodon.social from what it sees, and they can be short or empty for a while; `trending` + `links` sometimes returns fewer than 20 entries. The lists are served 20 (tags, links) or 40 (posts) entries per request and paged from there.
- **Post ids are server-local.** `threadReplies` needs a mastodon.social link or id; a link on another server is refused with a message. Use the `localUrl` column of any post row.
- **Hashtag search is capped at 40.** The source answers at most 40 hashtags per search and refuses paging without a login, so `maxItems` above 40 cannot be filled in `hashtagSearch`.
- **Accounts that opted out are skipped.** An account hosted on mastodon.social that asked not to be indexed is not exported in `accountPosts` or `profiles`; you get one row with `found: false` instead. Posts it puts under a hashtag are still read in `hashtagPosts`, because adding a hashtag is a request to be found.
- **No follower lists, no contact harvesting.** Only public posts, public profile text and the public follower/following/post **counts** are read. Follower and following lists, e-mail addresses and phone numbers are never collected. Media files are linked, never downloaded.
- **Engagement counts are a snapshot.** Favourites, boosts, replies and quotes are the values at `fetchedAt` and keep growing afterwards.
- **Rate limit.** The source allows 300 requests per five minutes per IP address for callers without a login, and Apify IPs are shared with other callers. The actor sends one request at a time, below one per second, and reads the remaining budget from every answer (`X-RateLimit-Remaining`, `X-RateLimit-Reset`): with 10 or fewer left it spreads its next requests over the rest of the window, and when the budget is used up (or the server answers HTTP 429) it waits for the window to reset if the run has time for that. If it has not, the run ends with the rows it has and the status "Stopped at the source's rate limit … run again after HH:MM UTC" — or fails with that message when nothing was written. It never switches IPs or User-Agents to get past the limit. A typical run needs 2–6 requests.
- **Your spending limit and the run timeout.** Rows are written as each page is read. When your **Maximum cost per run** is reached the run stops and ends with "Stopped at your spending limit: N rows delivered"; near the run timeout it stops starting new requests and ends with "Stopped before the run timeout: N rows saved". Rows already written stay, are billed once, and with `onlyNew` only the rows really written are remembered.
- **Empty results vs errors.** An unknown hashtag with no posts, a quiet account or an empty trend list is a successful run with a note in the status message; an unknown handle or deleted post gives one row with `found: false` and a reason; a source that misbehaves without producing a single row makes the run fail with that reason.
- **Why a post is missing a language.** Posts coming from other fediverse software often declare none. They are kept unless you set `languages`, which can only keep posts that declare one.
- **If the server closes these endpoints.** A Mastodon server can switch on authorized fetch, which would close the public API for anonymous callers. Should that happen, this actor is marked paused here instead of trying to work around it.

***

Made by **Yadroo**. Sibling actors: [bluesky-public-posts](https://apify.com/yadroo/bluesky-public-posts), [hackernews-search](https://apify.com/yadroo/hackernews-search), [stackexchange-search](https://apify.com/yadroo/stackexchange-search), [google-news-search](https://apify.com/yadroo/google-news-search), [rss-to-json](https://apify.com/yadroo/rss-to-json).

# Actor input Schema

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

Six questions, one per run. `hashtagPosts` is the topic feed: every public post tagged with the hashtags you name, newest first, and the only mode that takes tag combinations. `accountPosts` exports what named accounts publish. `threadReplies` turns one post into its discussion. `trending` answers "what is being talked about right now" as hashtags, shared news links or popular posts. `hashtagSearch` finds the hashtags that exist for a word and how much they are used - useful before you commit a monitoring run to one spelling. `profiles` resolves handles to follower, following and post counts. Each mode fills one dataset view; the other views stay empty.

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

Hashtags to read, with or without the leading `#`, case does not matter. Used by `hashtagPosts`; each hashtag is fetched separately and the row limit is split evenly over them, so two tags with a limit of 40 give about the last 20 posts of each. The tag that produced a row is written to `matchedHashtag`, so one dataset can hold several topics and still be filtered per topic.

## `alsoTagged` (type: `array`):

Widens the timeline: posts that carry the main hashtag OR any tag listed here. Sent to the source as its `any` parameter, so the widening happens before paging and costs no extra requests. Example: main tag `linux`, also tagged `opensource`, `foss`.

## `mustAlsoBeTagged` (type: `array`):

Narrows the timeline: only posts that carry the main hashtag AND every tag listed here. Sent to the source as its `all` parameter. Example: main tag `ai`, also tagged `opensource` returns the overlap of the two conversations - the cheapest way to watch a niche instead of a flood.

## `excludeTagged` (type: `array`):

Drops posts that also carry one of these tags, before they are fetched (the source's `none` parameter). Made for the common case where a topic tag is shared with a game, a giveaway or a bot stream you do not want in the dataset.

## `localOnly` (type: `boolean`):

Off by default, so a hashtag timeline shows the federated conversation: authors from any server that mastodon.social knows. Switch it on to keep only posts written on mastodon.social itself - a narrower, more homogeneous sample that is sometimes what a study of one community needs. Applies to `hashtagPosts`.

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

Accounts as `name` for mastodon.social accounts, `name@server` for accounts anywhere else in the fediverse, or as a profile link. Used by `accountPosts` and `profiles`. In `accountPosts` the row limit is split evenly over the accounts. An account that does not exist, was deleted or is unknown to this server yields one row with `found: false` and the reason in `notFoundReason`, so a typo in a long list never disappears silently.

## `excludeReplies` (type: `boolean`):

On by default: `accountPosts` then returns what the account published rather than the conversations it joined, which is what "what does this brand post" usually means. Switch it off for support or community accounts, where the replies are the interesting half. The source applies this before paging, so excluded replies are never fetched or charged.

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

An account timeline mixes in the posts it boosted (reblogged). They are dropped by default so rows are the account's own words. Switch this on to study what it amplifies: kept rows carry `isReblog: true`, the original author in `authorHandle` and the boosting account in `rebloggedBy`.

## `postUrls` (type: `array`):

Posts whose discussion you want, as mastodon.social links (`mastodon.social/@handle/<id>`) or as the bare numeric id this server uses. Used by `threadReplies`. Every post row of any mode carries `statusId` and `localUrl`, so a hashtag or account run can be chained into a thread run. A link on another fediverse server cannot be used directly - that server numbers its posts differently; take the `localUrl` of the same post instead. A deleted or unknown post yields one row with `found: false`.

## `includeAncestors` (type: `boolean`):

When the link points into the middle of a thread, add the posts it is a reply to (negative `replyDepth`), so the dataset reads from the start of the conversation. Switch off to get only the given post and what came after it.

## `trendType` (type: `string`):

`tags` gives one row per trending hashtag with how many accounts used it and how often, for each of the last seven days - feed the tag straight back into `hashtagPosts` to read the posts. `links` is the news monitor: the articles the network is sharing right now with title, description, publisher and per-day counts. `posts` returns the individual posts the server currently promotes.

## `searchQuery` (type: `string`):

Used by `hashtagSearch`: returns the hashtags whose name starts with or contains this word, each with its uses and distinct accounts per day for the last week. This is hashtag discovery, not post search - it answers "which spelling is actually used" (`climate`, `climatechange`, `climatecrisis`) before you spend a monitoring run on the wrong one.

## `languages` (type: `array`):

Keep only posts whose declared language is one of these, compared against the `language` column. Empty = every language. The language is declared by the app that posted, not detected by us, and a fair share of posts from other fediverse software carry none at all - those are dropped while this filter is set. Regional variants count as their language (`en-GB` is `en`), and Norwegian also keeps posts declared `no` or `nn` (Nynorsk). Applies to the post modes.

## `minFavourites` (type: `number`):

Keep only posts with at least this many favourites at fetch time - the quick way to see which posts under a hashtag actually travelled. Empty = no floor. Engagement counts are a snapshot and keep growing, which is why every row carries `fetchedAt`.

## `minReblogs` (type: `number`):

Same floor for boosts (reblogs). Boosts travel further than favourites on this network, so this is the better reach filter of the two.

## `excludeSensitive` (type: `boolean`):

Posts whose author flagged them as sensitive or put them behind a content warning are included by default and marked with `sensitive` and `contentWarning`. Switch this on for brand monitoring or anything that ends up on a shared dashboard.

## `onlyWithMedia` (type: `boolean`):

Ask the source for media posts only (its `only_media` parameter) in `hashtagPosts` and `accountPosts`. Rows then carry `mediaCount`, `mediaTypes` and `mediaUrls` with the file links the post published.

## `authorInstances` (type: `array`):

Keep only posts whose author lives on one of these fediverse servers, for example `mastodon.social`, `fosstodon.org`, `pixelfed.social`. Compared against the `authorInstance` column. Empty = every server. This is the filter a federated network needs and a single-server view cannot give you: watch one community inside a worldwide hashtag, or exclude a server that floods a tag.

## `sinceHours` (type: `number`):

Rolling window in hours, counted in UTC from the start of the run: 24 for a day, 168 for a week. This is the input for a schedule - it needs no date editing between runs. A hashtag or account timeline stops paging as soon as the source returns posts older than the window, so a narrow window is also a cheap run; the trending-posts list is ranked by popularity, so older posts there are dropped one by one instead.

## `onlyNew` (type: `boolean`):

Remember the ids written by this run in the actor's key-value store and, on the next run of the same actor or task, write only posts that were not written before. Made for monitoring: each run then holds just what is new, and an empty run means nothing was posted. The memory is kept per actor or task, so two tasks watching two hashtags do not interfere.

## `maxItems` (type: `number`):

Hard cap on the rows this run writes, and therefore on what it costs. Paging stops as soon as the cap is reached. In `hashtagPosts`, `accountPosts` and `profiles` the cap is divided over the hashtags or accounts you named. The run also stops at your Maximum cost per run, with the rows written so far.

## `fields` (type: `array`):

Keep only the named columns, in the order given - for example `authorHandle, text, favouriteCount, createdAt, url`. Empty = every column of the mode. `rowType`, `sourceMode` and the row's id column are always kept. Case does not matter and one near miss (`favoriteCount`) is corrected, with a note in the status message; a name no row has is reported and ignored, and a list in which no name exists fails the run with the valid column names.

## Actor input object example

```json
{
  "mode": "hashtagPosts",
  "hashtags": [
    "opensource",
    "linux"
  ],
  "localOnly": false,
  "accounts": [
    "Mastodon",
    "MastodonEngineering"
  ],
  "excludeReplies": true,
  "includeReblogs": false,
  "postUrls": [
    "https://mastodon.social/@Mastodon/117156549508722805"
  ],
  "includeAncestors": true,
  "trendType": "tags",
  "searchQuery": "climate",
  "excludeSensitive": false,
  "onlyWithMedia": false,
  "onlyNew": false,
  "maxItems": 50
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (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 = {
    "hashtags": [
        "opensource",
        "linux"
    ],
    "accounts": [
        "Mastodon",
        "MastodonEngineering"
    ],
    "postUrls": [
        "https://mastodon.social/@Mastodon/117156549508722805"
    ],
    "searchQuery": "climate"
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/mastodon-public-posts").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "hashtags": [
        "opensource",
        "linux",
    ],
    "accounts": [
        "Mastodon",
        "MastodonEngineering",
    ],
    "postUrls": ["https://mastodon.social/@Mastodon/117156549508722805"],
    "searchQuery": "climate",
}

# Run the Actor and wait for it to finish
run = client.actor("yadroo/mastodon-public-posts").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "hashtags": [
    "opensource",
    "linux"
  ],
  "accounts": [
    "Mastodon",
    "MastodonEngineering"
  ],
  "postUrls": [
    "https://mastodon.social/@Mastodon/117156549508722805"
  ],
  "searchQuery": "climate"
}' |
apify call yadroo/mastodon-public-posts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yadroo/mastodon-public-posts"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ucxxlzePP8KN2BHdx/builds/qC2HRb1utZ1aUvlEg/openapi.json
