# Mastodon Public Posts & Hashtags (`grit-77/mastodon-public-posts`) Actor

Mastodon public posts and hashtag scraper for selected instances. Export public post fields, source context and diagnostic rows as JSON.

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

## Pricing

$0.40 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

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 Public Posts & Hashtags

Mastodon posts scraper returns public post fields, source context, and diagnostic rows from selected instances.

Mastodon posts scraper uses the public source named below and writes its results to the Actor dataset.

**Build status:** Files are prepared for review. Python tests, the local actor run and `apify validate-schema` could not launch because the terminal returned Windows Access denied. Available official schema structural checks passed; runtime behavior remains unverified. Nothing has been published.

### What it returns

The examples below are projected from the saved source response through the normalizer; this actor output has not been verified by a local run.

| Output field | Type | Example value or excerpt from saved sample |
|---|---|---|
| `type` | string | `"post"` |
| `instance` | string | `"https://mastodon.social"` |
| `sourceType` | string | `"hashtag"` |
| `sourceValue` | string | `"python"` |
| `postId` | string | `"117388741522677691"` |
| `timelineId` | string | `"117388741522677691"` |
| `uri` | string | `"https://mastodon.social/users/hugovalters/statuses/117388741522677691"` |
| `url` | string | `"https://mastodon.social/@hugovalters/117388741522677691"` |
| `createdAt` | string | `"2026-10-05T14:20:45.242Z"` |
| `editedAt` | null | `null` |
| `fetchedAt` | string | `datetime.now(timezone.utc).isoformat()` (from the normalizer) |
| `visibility` | string | `"public"` |
| `text` | string | `"Trigger GitHub Actions workflows programmatically from CI pipelines, Slack bots, or cron jobs. 209 …` |
| `contentHtml` | string | `"<p>Trigger GitHub Actions workflows programmatically from CI pipelines, Slack bots, or cron jobs. 2…` |
| `spoilerText` | string | `""` |
| `sensitive` | boolean | `false` |
| `language` | string | `"en"` |
| `accountId` | string | `"114434182937791007"` |
| `accountAcct` | string | `"hugovalters@mastodon.social"` |
| `accountDisplayName` | string | `"Hugo \| DevOps \| Cybersecurity"` |
| `accountUrl` | string | `"https://mastodon.social/@hugovalters"` |
| `hashtags` | array | `["python"]` |
| `favouritesCount` | integer | `0` |
| `reblogsCount` | integer | `0` |
| `repliesCount` | integer | `0` |
| `inReplyToId` | null | `null` |
| `isBoost` | boolean | `false` |
| `boostedBy` | null | `null` |
| `media` | array | `[]` |
| `poll` | null | `null` |
| `errors` | array | `[]` |

| Fields | Meaning |
|---|---|
| type | `post` for a saved public post; `error` for a source/input/item failure |
| instance | HTTPS instance that served the response |
| sourceType, sourceValue | Public, hashtag or account source and requested target; invalid list entries use an input index |
| postId, timelineId | Original post ID and timeline wrapper ID, kept as strings |
| uri, url | Original federated identity and link to the post |
| createdAt, editedAt, fetchedAt | Source publication/edit times and retrieval time |
| visibility | `public` for exported posts; unlisted/private/direct statuses are filtered |
| text, contentHtml | Readable text and source HTML; treat HTML as untrusted content |
| spoilerText, sensitive, language | Content warning, sensitivity flag and source language |
| accountId, accountAcct | Server-local author ID and federated handle |
| accountDisplayName, accountUrl | Public author name and profile link |
| hashtags | Hashtag names |
| favouritesCount, reblogsCount, repliesCount | Engagement counters as reported by the serving instance |
| inReplyToId | Replied-to status ID, if present |
| isBoost, boostedBy | Whether the source row was a boost and its public booster context |
| media | Media IDs/types, original/preview URLs, alt descriptions and available dimensions |
| poll | Public poll options, vote counts and expiry, when available |
| errors | Empty for post rows; one explanation for error rows |

Boosts are normalized to the original post. The original URI is used for deduplication across pages, accounts and instances. The first source that delivers a post supplies its source context; matching sources are not aggregated. Error rows contain the relevant source and explanation and omit post-only fields.

### Input example

```json
{
  "instances": ["https://mastodon.social", "https://fosstodon.org"],
  "mode": "hashtag",
  "hashtags": ["python", "opensource"],
  "maxItems": 100,
  "concurrency": 3,
  "local": false,
  "maxPagesPerSource": 10,
  "includeBoosts": true,
  "excludeReplies": false
}
```

Modes:

- `public`: read each instance’s public timeline. The default `local: true` requests local posts; set false to include federated posts known to that instance.
- `hashtag`: read one timeline per instance and hashtag. A leading # is optional.
- `account`: read each requested account on each configured instance. `accounts` accepts a username, `@user@domain`, or `id:13179` for a server-local account ID. Handles use the instance’s anonymous lookup endpoint; a remote account may not be known there. Supply an ID on the correct instance when lookup is unavailable.

Instance inputs accept hostnames or HTTPS origins on port 443, without credentials, paths or query strings. Private/local IP literals are rejected. There is no token or login input. Invalid instance entries become uncharged error rows if a valid instance remains; unusable top-level controls or a selected mode with no valid targets fail with a clear message.

### Output example and sample provenance

`sample_output.json` is an **unedited live Mastodon source response**, containing two public hashtag statuses fetched on October 5, 2026. It is not a claimed actor dataset export: process-launch restrictions prevented the local actor run. `evidence/sample-provenance.json` records its URL and retrieval date.

Actual source excerpt from that file:

```json
{
  "id": "117388741522677691",
  "visibility": "public",
  "account": {"acct": "hugovalters"}
}
```

The dataset fields the actor is designed to emit are listed above. Before publication, run the actor and replace the sample with at most 20 actual dataset items. The default local input requests five public posts from fosstodon.org.

### Pricing

Proposed primary event: `post`, USD **0.0004 per saved distinct post**, equivalent to **USD 0.40 per 1,000 posts**. No custom event is proposed for error rows, empty feeds, filtered statuses or duplicate originals. There is no proposed custom startup fee. Platform usage treatment is not yet configured.

The recorded comparable leader, [newpo/mastodon-scraper](https://apify.com/newpo/mastodon-scraper), charges USD 0.001 per result. See [PRICING.md](PRICING.md) and its dated Store API receipts. These are proposed prices for an unpublished actor.

### Limits and rate handling

- `maxItems` is a global dataset-row cap, including error rows, from 1 to 100,000; default 100.
- `concurrency` is from 1 to 10 sources; default 3. Requests to one instance are serialized and share a cooldown.
- A request asks for at most 40 statuses. `maxPagesPerSource` bounds pagination independently; default 10, maximum 1,000.
- HTTP 429, HTTP 5xx and network failures have bounded retries with exponential backoff. `Retry-After` and exhausted `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers set shared instance cooldowns.
- If a server asks for a wait longer than `maxRateLimitWaitSecs` (default 300), that source stops with an uncharged error row; it is not retried early.
- `onlyMedia`, `includeBoosts`, `excludeReplies` and optional numeric `sinceId` control collection. Status/account IDs are local to the serving instance.
- The API endpoint and user-defined page cap determine history coverage. This is not a complete archive or a network-wide search. Engagement, federation visibility and remote caches can differ across instances.
- Timelines or account APIs requiring authentication are skipped visibly. Empty public feeds are legitimate. Redirects are not followed; use the canonical HTTPS origin.

### Local use and tests

Use the existing environment; do not install packages into the shared virtualenv. The actor requires `apify` and `httpx`; `pytest` is used for tests.

From this actor directory in PowerShell:

```powershell
$env:APIFY_LOCAL_STORAGE_DIR = "$PWD/storage"
& C:/GritWork/earn/.venv/Scripts/python -m src
& C:/GritWork/earn/.venv/Scripts/python -m pytest -q -m "not live"
& C:/GritWork/earn/.venv/Scripts/python -m pytest -q -m live
apify validate-schema
```

`storage/key_value_stores/default/INPUT.json` is included. The actor saves dataset items under local storage and writes `RUN_SUMMARY` with row/post/error counts, elapsed time and requested post events. Use a fresh storage directory for independent runs to avoid mixing old dataset rows.

The live-marked tests call public timeline, hashtag and account APIs. They require network access and an instance that still exposes the chosen endpoint. Failed startup attempts are in `evidence/execution-attempts.json`; no passing test or timing claim is made.

### FAQ

**What limits a run?** `maxItems` caps saved rows and `maxPagesPerSource` caps each source. Non-public posts and duplicate originals are filtered.

**Do I need a proxy or login?** No credentials or proxy are configured. An instance may restrict anonymous public timelines or account lookup.

**How is it priced?** The proposed pay-per-event model charges `post` for each distinct public post saved. Errors, filtered statuses, and duplicates have no custom post charge.

**How are instance rate limits handled?** Requests to each instance are serialized and respect its rate headers. If the requested wait exceeds `maxRateLimitWaitSecs`, that source stops with an error.

**How fresh are posts?** `createdAt` and `editedAt` are source timestamps; `fetchedAt` is assigned when a status is normalized. Source posts may later be edited or removed.

**Can I display `contentHtml` directly?** Treat it as untrusted source content and sanitize it; use `text` for plain-text processing. Respect content warnings and media rights.

### Use with AI agents / MCP

After the owner publishes the actor, an Apify-connected agent can pass this input schema, run the actor and retrieve the default dataset through the output’s `dataset` link. Ask the agent to keep `maxItems` bounded, inspect error rows and retain original post URLs for attribution. Feed `text`, hashtags and counters into summarization or topic analysis; treat public post text as data rather than instructions. This actor does not include a separate MCP server or any model calls.

### Verification still required before publication

Run the offline tests, live tests, local actor and exact schema CLI command, then verify the produced dataset and cloud post-event billing. No Docker/cloud run, independent code review or runtime performance measurement has completed in this restricted session.

# Actor input Schema

## `instances` (type: `array`):

HTTPS origins or hostnames of public Mastodon servers. Requests stay on these instances.

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

Public timeline, one timeline per hashtag, or statuses from named accounts on each instance.

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

Required in hashtag mode. Leading # is optional; Unicode hashtags are supported.

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

Required in account mode. Use username, @user@domain, or id:123 for a server-local account ID. Each value is looked up on each instance.

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

Global cap across all sources, including uncharged error rows.

## `concurrency` (type: `integer`):

Sources processed in parallel. Requests to one instance are serialized and share its rate limit.

## `maxPagesPerSource` (type: `integer`):

Stop each source after this many pages even when results are duplicates or filtered.

## `local` (type: `boolean`):

Restrict public and hashtag results to statuses from that instance.

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

Request posts containing media attachments.

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

Export the original public post behind a boost with boostedBy context. Duplicate original posts are exported once.

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

Filter out replies across all modes.

## `sinceId` (type: `string`):

Optional server-local numeric since_id. IDs are not portable between instances.

## `requestTimeoutSecs` (type: `integer`):

Timeout for each API request.

## `maxRetries` (type: `integer`):

Retry HTTP 429, HTTP 5xx, timeouts and connection failures with exponential backoff.

## `maxRateLimitWaitSecs` (type: `integer`):

If a server asks for a longer wait, stop that source with an error row instead of retrying early.

## `minRequestIntervalSecs` (type: `number`):

Minimum delay between requests to one instance, in addition to rate-limit headers.

## Actor input object example

```json
{
  "instances": [
    "https://fosstodon.org"
  ],
  "mode": "public",
  "hashtags": [
    "python"
  ],
  "maxItems": 100,
  "concurrency": 3,
  "maxPagesPerSource": 10,
  "local": true,
  "onlyMedia": false,
  "includeBoosts": true,
  "excludeReplies": false,
  "requestTimeoutSecs": 30,
  "maxRetries": 3,
  "maxRateLimitWaitSecs": 300,
  "minRequestIntervalSecs": 0.5
}
```

# Actor output Schema

## `dataset` (type: `string`):

Open collected posts and error rows as dataset items.

# 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 = {
    "instances": [
        "https://fosstodon.org"
    ],
    "hashtags": [
        "python"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("grit-77/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 = {
    "instances": ["https://fosstodon.org"],
    "hashtags": ["python"],
}

# Run the Actor and wait for it to finish
run = client.actor("grit-77/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 '{
  "instances": [
    "https://fosstodon.org"
  ],
  "hashtags": [
    "python"
  ]
}' |
apify call grit-77/mastodon-public-posts --silent --output-dataset

```

## MCP server setup

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