# Reddit Scraper & Monitor (`suslabs/reddit-scraper-monitor`) Actor

Reddit scraper and monitor without the API or RSS: posts, comments, search and communities from public Reddit pages. Scheduled monitors return only new items, with keyword filters, lead-intent scoring, presets and Slack/Discord/webhook alerts. Pay once per new item.

- **URL**: https://apify.com/suslabs/reddit-scraper-monitor.md
- **Developed by:** [SusLabs](https://apify.com/suslabs) (community)
- **Categories:** Social media, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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.
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

## Reddit Scraper & Monitor: posts, comments, keyword alerts

**Scrape Reddit posts, comments, search results, users and communities, or run a scheduled monitor that sends you only new posts, charged once.** It needs no Reddit API key, no login and no app approval, and it works without Reddit RSS.

- 🔁 **Read-only collection alternative for Reddit RSS and public API workflows.** Reddit ends RSS feeds on 13 November 2026 and phases out the public Data API through March 2027 ([Reddit changelog](https://support.reddithelp.com/hc/en-us/articles/54353370049684-Changelog-October-8-2026)). Map supported old `.rss` URLs to public-web collection, then adapt your n8n, Make, Zapier or Python workflow to the Actor's output. This does not emulate PRAW or authenticated API operations.
- 🆕 **Only new posts, every run.** A named monitor remembers what it already sent. Scheduled runs return only new posts and comments, and you pay for each item once. Already-seen and filtered items are free.
- 🎯 **Lead finding is included.** Free intent scoring flags people asking for a tool, an alternative or a recommendation, or naming a budget or your competitor, and shows which phrases matched. You can also add AI labels with your own OpenAI or Anthropic key.
- 🧩 **Ready-made presets.** Competitor alternatives, product complaints, feature requests, launch-thread watch, brand mentions and buying intent: give a product name and communities, and the Actor writes the full configuration and a cost estimate.
- 🔎 **Find the right subreddits.** Discovery mode suggests communities for a product or category, with evidence links and a relevance score, plus a ready-to-use monitor input.
- 🔔 **Alerts that retry.** Send a digest or one message per post to Slack, Discord, or any webhook (n8n, Make, Zapier). Failed alerts retry on later runs, each carries a delivery ID for deduplication, and an optional signature lets you verify it came from this Actor.
- 🧵 **Full threads with context.** It follows Reddit's "more comments" links and adds the post title and parent comment to every comment, so each one makes sense on its own for leads and AI. One output format gives each thread as a single markdown document for RAG.
- 💸 **Cheap to run hourly.** A $0.003 start fee plus $0.0015 per subreddit checked: an hourly 2-subreddit monitor costs about $4.32 a month before new posts. Posts are $2.00 per 1,000 on the Free plan, with lower prices on paid Apify plans.
- 🛟 **Built for recovery.** Recovery is designed and tested to suppress duplicate deliveries and charges. Partial and failed items are free. Every run writes a summary, an errors list and ready-to-run retry inputs.

| I want to… | Use |
| --- | --- |
| Get new posts from subreddits every hour | `mode: "monitor"` with `subreddits` |
| Find people asking for a tool like mine | a monitor with `keywordsAny`, `competitors` and intent scoring |
| Track links to my website | a monitor with `domains: ["yoursite.com"]` |
| Export a whole thread with comments | `mode: "post"` with `postUrls` |
| Search all of Reddit | `mode: "search"` with `queries` |
| Replace a dead Reddit RSS feed | put the `.rss` URL in `startUrls` |
| Catch complaints or feature requests about a product | `preset: "product-complaints"` or `"feature-requests"` |
| Find which subreddits discuss my category | `mode: "discovery"` with `productName` or `category` |

### Start with a small run

```json
{
  "mode": "subreddit",
  "subreddits": ["SaaS", "startups"],
  "sort": "new",
  "maxItems": 20,
  "maxPages": 1,
  "maxSeconds": 50,
  "intentScoring": true
}
```

Add `keywordsAny: ["AI agents", "alternative to"]` for local lead filtering.
Filtered items have no item fee; run-start and successful source-check fees apply.
Use `mode: "monitor"` and a stable `monitorName` for a persisted baseline.
First-run backfill defaults to seven days; subsequent runs return newly seen
fullnames. Explicitly watched post URLs also return changed scores/comment counts. Choose one name per input
scope. A concurrent run exits with `monitor-overlap` before collection.

### Collection modes

| Mode | Input | Behavior |
| --- | --- | --- |
| `subreddit` | `subreddits`, `sort`, `timeFilter` | Cheap full-text listing partials; hot/new/top/rising/controversial |
| `search` | `queries`, optional `restrictToSubreddits` | Posts, comments or community search; sitewide unless restriction is explicitly enabled |
| `post` | `postUrls` | Post plus bounded comment-tree expansion through site-provided partials |
| `user` | `users` | Visible public contributions; hidden profiles remain hidden |
| `community` | `subreddits`, optional discovery `queries` | Public descriptions, rules and exposed community metadata |
| `auto` | `startUrls` | Detect subreddit, search, post/comment and user URLs; map old `.rss` paths |
| `monitor` | `monitorName`, `subreddits`, optional `postUrls` | Cheap `/new` scans with local matching; watch-thread comments and score/count changes |

Search query syntax is passed unchanged to Reddit: quoted phrases, `AND`, `OR`,
`NOT`, `site:example.com` and `url:example.com/path` depend on the website's
support. Local keyword rules are literal Unicode whole-word/phrase matching,
case-insensitive, with `keywordsAny`, `keywordsAll`, `keywordsExclude` and
`matchedIn`. Comments found by a comment search (`searchType: "comments"`) are
always matched. Comments collected from threads participate in keyword/domain
matching only when `matchComments: true`. `domains` checks actual URL hosts and their subdomains.

Selected subreddits scope search queries only with `restrictToSubreddits: true`.
This prevents example subreddit names from quietly narrowing sitewide searches.
In monitor mode, selected subreddit listings and explicit queries are separate
sources. Leave `queries` empty for economical local keyword monitoring. Via the API,
an omitted `subreddits` list adds no subreddits when a monitor or search already has
queries or URLs; the SaaS/startups example is used only when nothing else is given. User profile URLs are skipped
by monitors; there is no persistent user tracking.

`postedAfter` / `postedBefore` accept ISO dates or relative values such as
`"1 hour"`, `"7 days"`. Other shared filters: `minScore`, `minComments`,
`flairInclude`, `flairExclude`, `includeNSFW` (false), and optional rough
`guessLanguage` (explicitly a heuristic, not reliable language identification).
NSFW flags never authorize passing an age gate. Private, quarantined, removed,
deleted and CAPTCHA-gated content is skipped.

`coverage: true` combines top/hot and time slices with dedupe. It increases
requests and cost; it cannot guarantee completeness past the site's caps.
`maxPages`, `maxComments`, `maxDepth`, `maxRequests`, response-byte and time
limits bound work. Reported `numComments` may include deleted, moderated or
Collection and recovery are bounded and tested on dated samples; source visibility and coverage can change.

Monitor listings use `monitorMaxPages` (default 20) instead of `maxPages`: they
continue until a page contains a seen fullname or the baseline backfill cutoff.
If a page/item/time/request safety cap stops the scan before that boundary,
SUMMARY.possibleGap and the final status warn you. Inspect RETRY_INPUTS and
increase the appropriate cap or run more often. Incomplete listing cards are
counted in SUMMARY.incompleteListingCards; they are not billed as full posts.
At most 2,500 items can be delivered per run. Continue large exports in chunks
using a named monitor or the supplied continuation input with excludeFullnames.

### Outputs and free features

Complete post rows include public body, title, author/flair when exposed,
timestamps, score, comment count, flags, linked URL/domain, media/thumbnail,
exposed awards and crosspost parent. Comments include thread context and parent
excerpt when available. Unavailable values are null; weekly activity counts
are never used as subscriber totals. `searchOutput: "cards"` returns explicitly
partial `type: "search-hit"` rows without pretending they contain full bodies.
Cards use the lower-priced `search-hit` event. Complete detail enrichment uses
the separate `detail-post` event. Focused comment enrichment from search/profile
pages uses `detail-comment`; bulk thread comments use `comment`.

Community search reads metadata from the fetched search cards and bills
`search-hit` per result. Its `type: "community"` rows have `partial: true`;
subscriber totals, creation dates and rules stay unavailable when the card does
not expose them. Use an explicit community URL for a separate metadata lookup.

Each row has schemaVersion, scrapedAt, matchedKeywords, changeType, charged and
chargeEvent. Each dataset row is a flat record with no embedded copy of itself.
Console views project relevant fields and omit charging internals; mixed-type
rows can have blank cells. Filter the top-level type field in your integration.

`outputFormat: "compact"` shortens bodies and marks truncation.
`"thread-markdown"` returns one free document per collected post for RAG, next to
the post and comment rows it is built from; those rows are billed as usual.
The default preserves complete public bodies. Incremental monitor mode rejects
thread-markdown: use default or compact so each new comment is a separate item.

On 22 October 2026, platform validation returned 20 prefill posts in 7.8 seconds.
An incremental repeat scan skipped all 48 observed items and returned no repeats
in 7.9 seconds. A comment run resumed after a real reboot and ended with 595
unique comments plus one post, with no duplicate delivery IDs. All observed cursors for the requested
public sort were exhausted; the site's reported comment total was larger.
These are dated samples, not guarantees for every community or future site.

Free deterministic intent scoring returns 0–100, matched phrases and points per
rule: recommendations, alternatives, pain, budget, requested features,
willingness to pay and configured competitors. Vendor promotion lowers the
score. Add `intentPhrases` or `competitors` to customize it. These are explainable
rules, not a promise that a person is a buyer.

Optional BYOK labels use `aiApiKey` (secret), `aiProvider`, `aiModel`,
`aiBaseUrl` (full compatible messages endpoint) and `aiMaxItems` (default zero).
The user's provider may charge separately. Failures do not prevent collection.

Optional secret `slackWebhookUrl`, `discordWebhookUrl` or `webhookUrl` sends
`digest` or capped `per-item` notifications (`webhookMaxItems`). Hooks include
title, subreddit, score, intent, keywords and URL. Delivery is best effort;
the dataset and recovery receipts remain the primary output.

### Migrate existing automations

| Previous workflow | Replacement input / wiring |
| --- | --- |
| n8n Reddit node | HTTP Request → this Actor with `mode: "monitor"`; map result fields |
| n8n RSS Read | Put its Reddit feed URL in `startUrls`; choose `auto`, or a named monitor |
| Make Reddit app | HTTP module → Actor run; iterate dataset rows |
| Zapier Reddit trigger | Scheduled Actor monitor → secret Catch Hook `webhookUrl` |
| PRAW listing/search script | Apify client run with `subreddits` or `queries`; read its dataset |

In n8n, use the Apify node (run this Actor, then read its dataset items) or an HTTP
Request to the `run-sync-get-dataset-items` endpoint. In Make, use the Apify app or an
HTTP module. In Zapier, point the Actor's `webhookUrl` at a Catch Hook. Test the
mapping with a small run before you schedule it.

Compared with moving-date-window collection, a named baseline keeps track of
delivered fullnames across runs. Overlap exclusion, local intent rules, recovery
journals and optional webhooks are included. Broad search and full trees still
depend on what the logged-out website exposes. The dated sample above describes
observed behavior, not a guarantee about every community or future site change.

### Call the Actor

Python (`pip install "apify-client>=3,<4"`):

```python
import os
from decimal import Decimal
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("suslabs/reddit-scraper-monitor").call(
    run_input={"mode": "subreddit", "subreddits": ["SaaS"], "maxItems": 20, "maxPages": 1},
    memory_mbytes=512, max_total_charge_usd=Decimal("0.05"))
if run is None or run.status != "SUCCEEDED":
    raise RuntimeError("Run did not succeed; check its status message and log.")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item["type"], item.get("title"), item["permalink"])
```

JavaScript (`apify-client`):

```javascript
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('suslabs/reddit-scraper-monitor').call(
  { mode: 'subreddit', subreddits: ['SaaS'], maxItems: 20, maxPages: 1 },
  { memory: 512, maxTotalChargeUsd: 0.05 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

curl (set APIFY_TOKEN in your shell; never put it in a shared document):

```sh
curl -X POST 'https://api.apify.com/v2/acts/suslabs~reddit-scraper-monitor/runs?memory=512&maxTotalChargeUsd=0.05' \
  -H "Authorization: Bearer $APIFY_TOKEN" -H 'Content-Type: application/json' \
  -d '{"mode":"subreddit","subreddits":["SaaS"],"maxItems":20,"maxPages":1}'
```

For AI agents, connect `https://mcp.apify.com?tools=suslabs/reddit-scraper-monitor` in any MCP client and ask:
“Run suslabs/reddit-scraper-monitor on new SaaS posts, at most 20 items and one
page, cap charges at $0.05, and return title, intent score and permalink.”
Configure the spending cap in the client/run options; prompts alone do not
enforce a budget.

### Billing, recovery and ownership

Pricing below describes the approved launch fee structure. Check the Store's
current Pricing tab before running; subscription-tier discounts may apply.

| Event | Free plan | Paid plans (Bronze → Gold+) | When it applies |
| --- | --- | --- | --- |
| apify-actor-start | $0.003 per run | same | Once per run at the default 512 MB |
| source-check | $0.0015 per source | same | Each subreddit, query, user, community or watched thread successfully checked (pagination included) |
| post | $2.00 / 1,000 | $1.85 → $1.50 / 1,000 | New complete listing or monitor posts |
| comment | $1.00 / 1,000 | $0.97 → $0.90 / 1,000 | New complete thread comments |
| detail-post | $3.50 / 1,000 | $3.40 → $3.00 / 1,000 | Complete posts opened from search or detail pages |
| detail-comment | $3.50 / 1,000 | $3.40 → $3.00 / 1,000 | Complete comments opened from search or profile pages |
| search-hit | $0.50 / 1,000 | $0.47 → $0.40 / 1,000 | Search result cards (title, link, subreddit, score; not the full text) and community-search results (name, description, weekly visitors) |

Free-tier in this table means the Apify subscription tier, not zero Actor fees.
Successful source checks are charged even when no new item matches. A failed,
restricted or unrecognized source page has no source-check charge; the run start
event still applies. Each logical source is charged at most once per run,
regardless of pagination. Standalone community metadata has no item fee;
community-search results use `search-hit`. An explicit community lookup is a
source. Automatically fetched metadata for a post does not
add another source fee. A query scoped to two subreddits is two source checks.
UPDATED watched-post rows, already-seen items, filtered rows and failed items
have no item fee. Intent scores, notification delivery and AI-label plumbing
add no Actor event. Your BYOK provider can charge separately.

`ACTOR_MAX_TOTAL_CHARGE_USD` is checked before target fetches and event delivery.
An hourly two-subreddit monitor over 30 days runs 720 times. If both sources
succeed each time, its fixed fees are
`720 × ($0.003 + 2 × $0.0015) = $4.32/month`, before new item fees.
At the Free-tier item rate, 1,000 new listing posts add $2.00, giving $6.32 total;
1,000 bulk-thread comments add $1.00. With no new items the $4.32 source/start
fees still apply. Add more sources, explicit search queries, runs or item types
to the calculation when your input includes them. Billing is in USD.

Read SUMMARY, ERRORS, RETRY_INPUTS, MONITOR_STATE and RUN_STATE alongside the
dataset. Complete durable receipts precede charging; confirmed charges precede
dataset projection so row metadata reflects the actual result. Cross-service
writes are not atomic: an uncertain charge or dataset POST stops for manual
reconciliation. Dataset POST failures are checked by deliveryId and only absent
rows are reposted, at most twice. ERRORS identifies any paid-but-undelivered
fullnames after those retries. Later monitor runs reconcile delivered/staged
receipts automatically and skip uncertain pending fullnames while continuing
other collection. Retention pruning is monotonic: lowering then raising
retentionDays does not make pruned content new again.
`resetMonitor` deliberately clears the baseline and allows a new backfill.

### Recipes for common jobs

#### SaaS founder: find people asking for alternatives

Start with community listings and local rules, so each scan does not need to
download detail pages for every search result. Choose communities where your
customers already discuss workflows. Leave search queries empty for this path.

```json
{
  "mode": "monitor",
  "monitorName": "saas-recommendations",
  "subreddits": ["SaaS", "startups"],
  "keywordsAny": ["alternative to", "looking for a tool", "willing to pay"],
  "keywordsExclude": ["hiring"],
  "competitors": ["Notion", "Airtable"],
  "firstRunBackfill": "1 day",
  "maxItems": 100,
  "monitorMaxPages": 10,
  "intentScoring": true
}
```

Schedule this input with the same monitorName. Route high-scoring results to
your own review queue, check the original conversation, and respond in context.
An intent score identifies matching phrases, not consent to contact someone or
a guarantee that they will buy. A first run may have a larger backfill than
later runs. Inspect possibleGap after unusually busy periods.

#### Marketer: monitor brand links and product mentions

Use literal local keywords for named products and domains for linked mentions.
Domain matching checks actual URL hosts, so `example.com.evil.test` does not
count as example.com. A domain filter requires a matching link, while keyword
rules can match text without a link. Use separate monitors if you want those
as alternative rather than combined conditions.

```json
{
  "mode": "monitor",
  "monitorName": "product-links",
  "subreddits": ["Entrepreneur", "SaaS"],
  "domains": ["example.com"],
  "firstRunBackfill": "7 days",
  "webhookMode": "digest",
  "webhookMaxItems": 20,
  "maxItems": 100
}
```

Set your secret webhookUrl in Console to send the digest to n8n, Make or Zapier.
For Slack/Discord use the matching secret input. Failed alerts do not remove
the dataset rows. Digests are capped; read the dataset for the full result set.
Resetting a monitor intentionally starts a new backfill and can deliver items
again, so it should not be part of your normal hourly schedule.

#### Researcher: collect an explicitly bounded public thread

Use post mode with a full public permalink, then set the requested comment sort,
depth and count. The Actor follows the website's top-level comment seed and
read-only more-comments cursor forms. It never recovers removed text or enters
restricted content to make a reported count match an export count.

```json
{
  "mode": "post",
  "postUrls": ["https://www.reddit.com/r/SaaS/comments/POST_ID/POST_SLUG/"],
  "commentSort": "new",
  "expandComments": true,
  "maxComments": 500,
  "maxDepth": 20,
  "maxPages": 100,
  "maxItems": 501,
  "maxSeconds": 240
}
```

Replace the placeholder with the real permalink. A post and its comments are
separate items in default format. SUMMARY.threadCoverage reports the observed
cursor graph, remaining partials and truncation. Zero observed cursors means
all branches exposed for that sort were visited; unavailable, deleted or capped
comments can still make the website's displayed total larger.

For ongoing replies, switch to monitor with a stable monitorName, empty
subreddits and the watched postUrls. Newly observed comments are delivered once.
Changes to the watched post's score/count generate free UPDATED rows with
changedFields. Start and successful source-check fees still apply.

#### AI developer: context-rich discussions for retrieval

Default comment rows contain postTitle, postUrl, parentId, depth and a parent
excerpt when exposed. This makes each comment useful outside the original page.
Compact format reduces body/media size; it explicitly marks truncated bodies.
Use full default bodies when training or analysis needs the complete public text.

```json
{
  "mode": "post",
  "postUrls": ["https://www.reddit.com/r/SaaS/comments/POST_ID/POST_SLUG/"],
  "outputFormat": "thread-markdown",
  "commentSort": "new",
  "maxComments": 200,
  "maxPages": 40,
  "maxItems": 1
}
```

This produces one detail-post document with source URL, post body and bounded
comment tree. The document itself is free; its post and comments are billed as rows.
It is an explicit one-shot export, not an incremental monitor format. Store
source URLs with your derived documents, apply your own retention rules and
respect the authors' rights. Optional JSONL fine-tuning is not provided.

#### Search: broad discovery before enrichment

```json
{
  "mode": "search",
  "queries": ["\"alternative to\" AND notion", "site:github.com automation"],
  "restrictToSubreddits": false,
  "searchType": "posts",
  "searchOutput": "cards",
  "sort": "new",
  "timeFilter": "week",
  "maxItems": 50,
  "maxPages": 3
}
```

Cards contain the public teaser metadata and are marked partial. They use the
search-hit event rather than a complete-post price. Switch searchOutput to
complete to fetch full detail pages. Comment search uses searchType comments;
complete comment enrichment is billed as detail-comment. In search monitors,
already-seen cards are checked against the baseline before downloading details.
The website controls which search syntax and sorts work, and can cap results.

### Presets, discovery, analysis and alerts

Use this Actor for read-only public discussion collection. It does not post, vote,
moderate or replace authenticated PRAW operations. Logged-out visibility can change.

#### Start with a preset

Set `preset` to `find-alternatives-to-competitor`, `product-complaints`,
`feature-requests`, `launch-thread-watch`, `brand-and-domain-mentions` or
`buying-intent-leads`. Supply `productName`, `competitors`, `keywords`,
`communities` and an optional destination. A thread watch needs `postUrls`.
`EFFECTIVE_CONFIG` shows the expanded configuration with secrets redacted.
`COST_ESTIMATE` explains fixed fees at hourly/daily/weekly frequency and the
per-item rates. Choosing a frequency does not create a schedule.

```json
{"preset":"product-complaints","productName":"YourProduct","communities":["SaaS"],"monitorName":"product-feedback","maxItems":50,"scheduleFrequency":"daily"}
```

The complaint/feature/alternative/buying presets apply deterministic qualification
rules after keyword selection. Wording outside those rules can be missed. Review
your configuration and broaden keywords when necessary.

#### Discover relevant communities

```json
{"mode":"discovery","category":"invoicing software","maxItems":30}
```

Search evidence cards are billed as `search-hit`, with `source-check` per successful
logical query. Free derived `discovery` rows report matching-card counts, observed
recency, sample links and a heuristic relevance score. Subscriber counts stay null
when the cards do not expose them. `DISCOVERY_MONITOR_INPUT` is ready to copy into a
monitor; the Actor does not create or publish a task for you.

#### Explainable analysis

Local analysis includes complaint and feature-request evidence, question detection,
product/competitor mentions and short extractive summaries. `sentimentAnalysis:true`
adds English VADER sentiment. These are deterministic heuristics, not model-verified
facts or sales probabilities. An unanswered Q\&A filter means no OP reply was observed
in the fetched graph. Optional BYOK labels support up to five `customLabels` prompts;
your provider charges your key. Local analysis adds no PPE event.

#### Persistent alerts and replay

Named monitors persist alerts before sending. Failed requests retry with backoff on
later runs, up to `alertMaxAttempts`; terminal states and retention limits are visible.
Delivery is at least once: receivers should deduplicate `X-SusLabs-Delivery-Id`.
Set secret `webhookSecret` to receive `X-SusLabs-Signature: sha256=<hex>` over
`deliveryId + "." + timestamp + "." + exact HTTP body bytes`; timestamp is supplied in
`X-SusLabs-Timestamp`. Reject stale timestamps and compare signatures in constant time.
`webhookAuthorization` optionally supplies a secret Authorization header.

`webhookMode` selects capped per-item requests or digests of up to ten records.
Slack/Discord text is bounded; read the dataset for full text. `webhookMaxItems`
limits delivery attempts per run, leaving remaining alerts queued. Retention is
`alertRetentionDays` (default seven days); retained terminal history is also bounded
to fit a 4 MB outbox, so old replay history can expire earlier. Destination URLs and
secrets must be supplied again on retry/replay; only a destination hash is retained.

```json
{"mode":"monitor","monitorName":"product-feedback","subreddits":["SaaS"],"keywordsAny":["YourProduct"],"replayAlerts":{"since":"2026-10-22T00:00:00Z"},"webhookUrl":"YOUR_SECRET_WEBHOOK_URL"}
```

Replay reads retained alerts without Reddit collection: only the automatic Actor
start event applies. Only the monitor name, destination credentials and selection
are needed; source inputs are ignored during replay. Replay can repeat
an alert intentionally; the delivery ID stays the same.

#### Source freshness and catch-up

SUMMARY, MONITOR_STATE and the sourceHealth dataset view expose last successful
check, last attempt, consecutive failures, route used and interrupted coverage.
`staleSourceHours` sets when a source with no successful check is reported: one
status row and one alert per stale episode, not one per run. Monitor datasets get
`source-health` rows only for stale or failing sources, and `alert-delivery` rows only
for alerts that failed or expired; the full picture is in SUMMARY and MONITOR_STATE.
Persisted continuations are attempted before fresh work on the next run; one that
makes no progress for three runs is dropped and reported as a gap. Recovery is bounded
by source visibility and configured caps; inspect possibleGap and errors.

#### Bulk work and Q\&A

`postUrlsFile` and `startUrlsFile` accept public HTTPS CSV/TXT files (first column,
optional URL header), at most 1 MB and 5,000 entries. Imports run on Apify with
private-network protection. `qaFilter` is all/answered/unanswered; it reflects
observed OP replies, not exhaustive answer coverage. `rootCommentId` and edit
timestamps are populated only when exposed or derivable from observed relationships.

Large requested jobs run in bounded chunks. RETRY_INPUTS contains delivered-ID
exclusions and remaining requested work; reattach secret destinations and keys.

Thread Markdown is a derived free `thread-document` beside the individually billed
post/comment component rows. Embedded comments are not a free scraping path. Filter
on type when importing only documents. All component rows count toward maxItems.

#### Persona recipes

| Team | Start with | Useful result |
| --- | --- | --- |
| SaaS founders | buying-intent-leads or competitor alternatives | Ranked discussions to review and answer manually |
| Product/support | complaints, feature requests, launch thread | New feedback, observed changes and reply context |
| Agencies/automation | named brand/domain monitor | Reusable state and structured alert delivery |
| Researchers/RAG | public thread URLs, Markdown, flat exports | Bounded discussion records with source links |

### FAQ

**Does it require a Reddit account or API credentials?**
No. It uses public logged-out website pages and anonymous session state issued
by the site. Login, OAuth/Data API credentials, real-user cookies, CAPTCHA
solving, private communities and identity enrichment are excluded.

**Can I paste an RSS URL?**
Yes. Auto mode maps recognized old subreddit, search, post and user RSS paths
to web collection inputs. It never fetches RSS as a fallback. A legacy comment
feed maps to website comment search; website search availability and caps apply.
The mapping does not preserve unsupported RSS query options or guarantee the
same ordering as the old feed.

**Why did a run with zero results still have fees?**
Run-start and successful source-check fees pay for the scan. No item event is
charged for seen, filtered or failed candidates. For example, a successful
two-subreddit empty scan has $0.006 in approved start/source fees. A source that
fails after bounded retries has no source-check fee; the run start still applies.

**Will changing scores re-charge a post?**
Normal listing monitors do not re-deliver score changes. Explicit watched-thread
monitors return a free UPDATED post row with changedFields. NEW complete posts
and comments have their ordinary item events. Changing/resetting your monitor
baseline is a deliberate new scope and can cause a fresh charged backfill.

**Can a busy community have more new posts than the limit?**
Yes. Monitor pagination searches for a seen/cutoff boundary up to its safety
cap. Item, page, byte and time limits still matter. A possibleGap warning means
you should continue from RETRY_INPUTS, increase a cap or schedule more often.
If every page is already familiar the run usually finishes much sooner.

**Are null fields errors?**
Not necessarily. Hidden scores, missing edit timestamps, author flair or
subscriber totals remain null when the public source does not expose them.
Presence flags on shreddit posts are parsed as booleans. An NSFW toggle never
authorizes bypassing a mature-content gate. Unknown NSFW flags on comment-search
cards are excluded unless you opt in.

**Are retries exactly once across all services?**
No distributed transaction spans charging, KV storage and datasets. Stable
delivery IDs, charge idempotency and batch journals make interrupted work
recoverable. Dataset failures trigger an ID scan before at most two reposts.
Late ambiguous charges remain pending for review; their fullnames are skipped
while other monitor collection continues. Always read SUMMARY and ERRORS.

**Do integrations need credentials?**
Your own Slack/Discord/webhook or automation service may need its own connection.
The included n8n/Make examples are inactive templates. Import, select your
connections, test field mappings and enable them yourself. No credentials are
included. The Apify token belongs in your secret/environment configuration.

**How do I export more than 2,500 items?**
Use multiple bounded runs. Named monitors retain their baseline; one-shot
continuations exclude delivered fullnames. Save the dataset ID and continuation
input after each run. Increasing a requested total cannot remove logged-out
site caps or make unavailable comments accessible.

Reddit content belongs to its authors. Users are responsible for their use,
retention and redistribution. This Actor accesses public logged-out content;
it does not provide identity enrichment or private content access.

# Changelog

This Actor's version history is a separate document: https://apify.com/suslabs/reddit-scraper-monitor/changelog.md

# Actor input Schema

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

All runs: select public collection or persistent monitoring.

## `subreddits` (type: `array`):

Subreddit, search, community and monitor modes: plain community names. Monitor listings always scan new.

## `queries` (type: `array`):

Search/community discovery, or explicit search in monitor mode: Reddit Boolean/quoted query syntax. Empty for cheap local-keyword monitoring.

## `users` (type: `array`):

User mode: visible public contributions only; no identity enrichment or hidden profiles.

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

Post/auto/monitor modes: public post or comment permalinks; monitor watches new comments and post changes.

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

Auto and collection modes: public subreddit, search, user or post URLs. Old RSS paths are mapped without fetching RSS.

## `sort` (type: `string`):

Listing/search modes: website sort. Monitor subreddit scans always use new; thread sort is commentSort.

## `timeFilter` (type: `string`):

Listing/search modes: website time range, subject to its supported sort combinations.

## `searchType` (type: `string`):

Search mode: posts, comments or community discovery.

## `searchOutput` (type: `string`):

Search: complete enriches public details; cards returns explicitly partial search-hit rows, billed at the separate search-hit event.

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

Requested job size. Each run currently accepts at most 2500 delivered items; RETRY_INPUTS contains the remaining input and delivered-ID exclusions. See dated platform evidence.

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

Collection modes: maximum pages per source. Monitor listings use monitorMaxPages and continue to the baseline boundary.

## `maxComments` (type: `integer`):

Post URLs and watched threads: maximum comments per thread.

## `maxDepth` (type: `integer`):

Post URLs and watched threads: maximum included comment depth.

## `expandComments` (type: `boolean`):

Post URLs and watched threads: follow the site-provided comment cursor forms.

## `commentSort` (type: `string`):

Post URLs, watched threads and enriched search: public comment-tree sort.

## `includeComments` (type: `boolean`):

Search post enrichment only: also collect that post's comments. Direct post mode includes comments by default.

## `includeNSFW` (type: `boolean`):

Collection modes: include public NSFW flags/content only where logged-out access permits it. Never passes a login or age gate.

## `coverage` (type: `boolean`):

Collection and monitor modes. coverage: applies to collection modes unless a narrower mode is stated in README.

## `postedAfter` (type: `string`):

Collection and monitor modes. postedAfter: applies to collection modes unless a narrower mode is stated in README.

## `postedBefore` (type: `string`):

Collection and monitor modes. postedBefore: applies to collection modes unless a narrower mode is stated in README.

## `minScore` (type: `integer`):

Collection and monitor modes. minScore: applies to collection modes unless a narrower mode is stated in README.

## `minComments` (type: `integer`):

Collection and monitor modes. minComments: applies to collection modes unless a narrower mode is stated in README.

## `flairInclude` (type: `array`):

Collection and monitor modes. flairInclude: applies to collection modes unless a narrower mode is stated in README.

## `flairExclude` (type: `array`):

Collection and monitor modes. flairExclude: applies to collection modes unless a narrower mode is stated in README.

## `keywordsAny` (type: `array`):

Collection and monitor modes. All collection/monitor modes: Unicode whole-word phrases, any match; applied locally to cheap subreddit listings.

## `keywordsAll` (type: `array`):

Collection and monitor modes. keywordsAll: applies to collection modes unless a narrower mode is stated in README.

## `keywordsExclude` (type: `array`):

Collection and monitor modes. keywordsExclude: applies to collection modes unless a narrower mode is stated in README.

## `matchComments` (type: `boolean`):

Keyword/domain rules: include comment bodies in matching; off by default.

## `domains` (type: `array`):

Collection and monitor modes. domains: applies to collection modes unless a narrower mode is stated in README.

## `monitorName` (type: `string`):

Monitor only: stable name for a persisted baseline. Same-name runs are serialized with a platform lease.

## `firstRunBackfill` (type: `string`):

Monitor only: ISO/relative cutoff anchored to baseline creation, such as 7 days.

## `retentionDays` (type: `integer`):

Monitor only: prune old entries and exclude content older than retention from later delivery.

## `resetMonitor` (type: `boolean`):

Monitor only: intentionally discard the named baseline and allow a fresh backfill.

## `outputFormat` (type: `string`):

Collection modes: default or compact. thread-markdown requires explicit post URLs and includes embedded comments without extra comment events.

## `includeHtml` (type: `boolean`):

Collection and monitor modes. includeHtml: applies to collection modes unless a narrower mode is stated in README.

## `guessLanguage` (type: `boolean`):

Collection and monitor modes. guessLanguage: applies to collection modes unless a narrower mode is stated in README.

## `intentScoring` (type: `boolean`):

Collection and monitor modes. intentScoring: applies to collection modes unless a narrower mode is stated in README.

## `competitors` (type: `array`):

Collection and monitor modes. competitors: applies to collection modes unless a narrower mode is stated in README.

## `intentPhrases` (type: `object`):

Collection and monitor modes. intentPhrases: applies to collection modes unless a narrower mode is stated in README.

## `aiProvider` (type: `string`):

Collection and monitor modes. aiProvider: applies to collection modes unless a narrower mode is stated in README.

## `aiApiKey` (type: `string`):

Secret. Optional AI labeling or webhook delivery only; never echoed in outputs.

## `aiBaseUrl` (type: `string`):

Optional BYOK labeling: full HTTPS compatible messages endpoint, not a base prefix.

## `aiModel` (type: `string`):

Collection and monitor modes. aiModel: applies to collection modes unless a narrower mode is stated in README.

## `aiMaxItems` (type: `integer`):

Optional BYOK labeling: cap attempted items per run; zero disables calls.

## `slackWebhookUrl` (type: `string`):

Secret. Optional AI labeling or webhook delivery only; never echoed in outputs.

## `discordWebhookUrl` (type: `string`):

Secret. Optional AI labeling or webhook delivery only; never echoed in outputs.

## `webhookUrl` (type: `string`):

Secret. Optional AI labeling or webhook delivery only; never echoed in outputs.

## `webhookMode` (type: `string`):

Optional delivery in collection/monitor modes: one digest or capped per-item notifications.

## `webhookMaxItems` (type: `integer`):

Optional delivery: maximum item alerts; notifications also share a 15-second run time budget.

## `maxSeconds` (type: `integer`):

Collection and monitor modes. maxSeconds: applies to collection modes unless a narrower mode is stated in README.

## `maxRequests` (type: `integer`):

Collection and monitor modes. maxRequests: applies to collection modes unless a narrower mode is stated in README.

## `maxResponseBytes` (type: `integer`):

Collection and monitor modes. maxResponseBytes: applies to collection modes unless a narrower mode is stated in README.

## `excludeFullnames` (type: `array`):

Collection and monitor modes. All collection modes. Recovery inputs use these fullnames to avoid replaying delivered rows.

## `restrictToSubreddits` (type: `boolean`):

Search and monitor queries: only restrict queries to subreddits when enabled. Otherwise queries are sitewide; monitor listing sources still use selected subreddits.

## `monitorMaxPages` (type: `integer`):

Monitor mode: page until a baseline fullname or cutoff is reached, at most this many pages per listing. SUMMARY.possibleGap warns if a cap stops collection first.

## `preset` (type: `string`):

Expands into a reviewable monitor configuration in EFFECTIVE_CONFIG. No schedule is created.

## `productName` (type: `string`):

Optional productName. See README for behavior.

## `category` (type: `string`):

Optional category. See README for behavior.

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

Optional keywords. See README for behavior.

## `communities` (type: `array`):

Optional communities. See README for behavior.

## `destination` (type: `object`):

Optional preset destination object: type slack, discord or webhook. Prefer the corresponding secret URL input.

## `scheduleFrequency` (type: `string`):

Optional scheduleFrequency. See README for behavior.

## `staleSourceHours` (type: `integer`):

Optional staleSourceHours. See README for behavior.

## `alertMaxAttempts` (type: `integer`):

Optional alertMaxAttempts. See README for behavior.

## `alertRetentionDays` (type: `integer`):

Optional alertRetentionDays. See README for behavior.

## `webhookSecret` (type: `string`):

Secret; optional. Not echoed in effective configuration or continuations.

## `webhookAuthorization` (type: `string`):

Secret; optional. Not echoed in effective configuration or continuations.

## `replayAlerts` (type: `object`):

Stored alerts only: {"since":"2026-10-22T00:00:00Z"} or {"deliveryIds":\["sla\_..."]}. No Reddit requests; automatic start fee only.

## `analysisEnabled` (type: `boolean`):

Optional analysisEnabled. See README for behavior.

## `sentimentAnalysis` (type: `boolean`):

Optional sentimentAnalysis. See README for behavior.

## `customLabels` (type: `object`):

Optional customLabels. See README for behavior.

## `postUrlsFile` (type: `string`):

Secret; optional. Not echoed in effective configuration or continuations.

## `startUrlsFile` (type: `string`):

Secret; optional. Not echoed in effective configuration or continuations.

## `qaFilter` (type: `string`):

Observed OP reply within the fetched public graph; unanswered is not proof no answer exists.

## `discoveryMaxCommunities` (type: `integer`):

Optional discoveryMaxCommunities. See README for behavior.

## Actor input object example

```json
{
  "mode": "subreddit",
  "subreddits": [
    "SaaS",
    "startups"
  ],
  "queries": [],
  "users": [],
  "postUrls": [],
  "startUrls": [],
  "sort": "new",
  "timeFilter": "all",
  "searchType": "posts",
  "searchOutput": "complete",
  "maxItems": 20,
  "maxPages": 1,
  "maxComments": 1000,
  "maxDepth": 20,
  "expandComments": true,
  "commentSort": "confidence",
  "includeComments": false,
  "includeNSFW": false,
  "coverage": false,
  "flairInclude": [],
  "flairExclude": [],
  "keywordsAny": [],
  "keywordsAll": [],
  "keywordsExclude": [],
  "matchComments": false,
  "domains": [],
  "monitorName": "",
  "firstRunBackfill": "7 days",
  "retentionDays": 30,
  "resetMonitor": false,
  "outputFormat": "default",
  "includeHtml": false,
  "guessLanguage": false,
  "intentScoring": true,
  "competitors": [],
  "intentPhrases": {},
  "aiProvider": "openai",
  "aiBaseUrl": "",
  "aiModel": "",
  "aiMaxItems": 0,
  "webhookMode": "digest",
  "webhookMaxItems": 20,
  "maxSeconds": 50,
  "maxRequests": 150,
  "maxResponseBytes": 60000000,
  "excludeFullnames": [],
  "restrictToSubreddits": false,
  "monitorMaxPages": 20,
  "preset": "",
  "productName": "",
  "category": "",
  "keywords": [],
  "communities": [],
  "destination": {},
  "scheduleFrequency": "daily",
  "staleSourceHours": 24,
  "alertMaxAttempts": 5,
  "alertRetentionDays": 7,
  "replayAlerts": {},
  "analysisEnabled": true,
  "sentimentAnalysis": false,
  "customLabels": {},
  "qaFilter": "all",
  "discoveryMaxCommunities": 10
}
```

# Actor output Schema

## `summary` (type: `string`):

Run totals, actual charged counts, network attempts and stop reason.

## `errors` (type: `string`):

Sanitized failures; secret values are excluded.

## `retry_inputs` (type: `string`):

Standalone runnable inputs for undelivered jobs.

## `monitor_state` (type: `string`):

Named baseline store ID and pending count.

## `run_state` (type: `string`):

Durable delivery receipts. Reconcile uncertain writes before manual recovery.

## `route_health` (type: `string`):

Daily canary results; also saved in the named route-health store.

## `items` (type: `string`):

Public complete records and explicitly partial search hits.

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

Type-specific projection; billing metadata is omitted.

## `comments` (type: `string`):

Type-specific projection; billing metadata is omitted.

## `communities` (type: `string`):

Type-specific projection; billing metadata is omitted.

## `searchHits` (type: `string`):

Type-specific projection; billing metadata is omitted.

## `effective_config` (type: `string`):

No description

## `cost_estimate` (type: `string`):

No description

## `discovery_monitor_input` (type: `string`):

No description

## `sourceHealth` (type: `string`):

No description

## `analysis` (type: `string`):

No description

## `discovery` (type: `string`):

No description

## `alerts` (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 = {
    "subreddits": [
        "SaaS",
        "startups"
    ],
    "maxPages": 1,
    "keywordsAny": [],
    "maxSeconds": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("suslabs/reddit-scraper-monitor").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 = {
    "subreddits": [
        "SaaS",
        "startups",
    ],
    "maxPages": 1,
    "keywordsAny": [],
    "maxSeconds": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("suslabs/reddit-scraper-monitor").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 '{
  "subreddits": [
    "SaaS",
    "startups"
  ],
  "maxPages": 1,
  "keywordsAny": [],
  "maxSeconds": 50
}' |
apify call suslabs/reddit-scraper-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,suslabs/reddit-scraper-monitor"
        }
    }
}
```

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/c3pZJBbpVUNkRNsWS/builds/RtpQVuqaPJefbvkBF/openapi.json
