# Reddit Posts Scraper by Keyword Score Filter (`scraper-engine/reddit-posts-scraper`) Actor

Scrape Reddit posts from any subreddit with ease. This actor collects titles, upvotes, timestamps, authors, comment counts, and post URLs. Perfect for research, trend tracking, content analysis, and automations that rely on fresh Reddit data.

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

## Pricing

$15.99/month + usage

To use this Actor, you pay a monthly rental fee to the developer. The rent is subtracted from your prepaid usage every month after the free trial period.You also pay for the Apify platform usage, which gets cheaper the higher Apify subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#rental-actors

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### Reddit Posts Scraper — Keyword Filter, Scores, Flair and Comments

Reddit Posts Scraper pulls public Reddit posts from subreddits, Reddit URLs and search terms, then keeps only the ones matching your keyword watchlist and clearing a minimum `score`. Every row carries `title`, `body`, `author`, `score`, `num_comments`, `linkFlairText` and a nested `comments` tree, plus the filter provenance that produced it. Structured JSON, no HTML parsing, no Reddit account or API key. Paste a subreddit, set a keyword and a score floor, press Start.

### ⚠️ A one-word source is a subreddit, not a search

`startUrls` decides what each line means by looking at it, and the rule is mechanical: a line starting with `http` is a URL, a line starting with `r/` is a subreddit, **a line containing no space and no `+` is a subreddit**, and everything else runs as a Reddit search. So `artificial intelligence` searches Reddit, but `layoffs` fetches `r/layoffs` — a real subreddit that is not what you meant. To search on a single word, add a `+`: `layoffs+2026`, or use two words.

Two related consequences worth knowing before your first run:

- A **Reddit search-results URL** pasted into `startUrls` is classified as a URL, not a search, and is then read with the listing parser — which looks for post containers that a search page does not have. It returns zero posts. Enter the search phrase itself instead.
- Only URLs of the exact form `/r/<name>` get your **Sort order** and **Time range** appended. A deeper URL is fetched exactly as you typed it, so `sortOrder` and `timeFilter` are silently ignored for it.

### What is Reddit Posts Scraper?

Reddit Posts Scraper is an Apify Actor that reads public Reddit listing, search and comment pages from `old.reddit.com` as HTML and flattens each post into one typed dataset row, with its top-level comments and their replies nested inside it. On top of the raw post it applies a social-listening layer: a case-insensitive keyword watchlist with four match modes, a minimum-score floor and an NSFW exclusion.

**No Reddit account, login, OAuth app or API key is required.** The Actor never authenticates. The only cookie it sends is `over18=1`, which is the age confirmation any logged-out visitor clicks through — it is not a session.

It is built for social-listening and brand-monitoring teams, market researchers tracking how communities discuss a topic, and developers piping Reddit discussion text into dashboards or AI pipelines.

### What Reddit post data is publicly available to scrape?

A logged-out Reddit listing page carries the post title, the author's username and stable account id, the score, the comment count, the timestamp, the flair, the link domain and outbound URL, the crosspost count, and the full body text of self posts. Comment threads on a public post are equally open. Everything tied to an account — votes, saves, messages, mod queues — is not.

| Data Category | Publicly Available | Gated behind a Reddit account or not exposed in HTML |
| ----- | ----- | ----- |
| Post title, author, score, comment count, timestamp, subreddit | ✅ Public | — |
| Self-post body text, flair, link domain, outbound URL, crosspost count | ✅ Public | — |
| Top-level comments and their nested replies, with author and score | ✅ Public | — |
| NSFW posts and NSFW subreddits | ✅ Public behind an age interstitial | The Actor sends the `over18=1` confirmation cookie — no account involved |
| Upvote ratio | ❌ | Reddit publishes it in the `.json` API response, not in the `old.reddit.com` HTML this Actor reads — not returned |
| Awards and gallery image sets | ❌ | Not parsed and not returned |
| Private, quarantined or banned subreddits | ❌ | Not accessible without an account, or not at all |
| Your votes, saved posts, messages, moderator queue | ❌ | Behind a Reddit login — never returned |
| An author's real name, email or any contact detail | ❌ | Reddit does not publish it |

Reddit Posts Scraper only returns publicly visible data — what any visitor sees. Nothing behind a login wall.

### What data can I extract with Reddit Posts Scraper?

Every charged row describes one Reddit post and carries up to **30 keys**: post identity and text, engagement metrics, safety and link metadata, and a record of exactly which filters let the row through. Comments arrive as a nested array of objects with **5 further keys** each, replies included.

| Field Name | Description |
| ----- | ----- |
| `post_id` | Reddit's base-36 post id, taken from the `t3_` fullname. Always present on charged rows |
| `title` | Post title. `"No Title"` when the title link cannot be read |
| `author` | Author's username. `"Unknown"` when Reddit omits the author attribute |
| `authorFullname` | Reddit's stable `t2_` account id for the author. Omitted when not exposed |
| `subreddit` | Subreddit name without the `r/` prefix |
| `subredditType` | Subreddit visibility as Reddit labels it, defaulting to `"public"` |
| `body` | Full self-post text, flattened to plain text. `""` for link posts and for every search-sourced row |
| `score` | Net upvote score as Reddit displayed it at fetch time |
| `num_comments` | Comment count Reddit displayed on the listing |
| `numCrossposts` | Number of crossposts. `0` on search-sourced rows |
| `created_utc` | Post creation time as a Unix epoch in seconds. Omitted when unreadable |
| `publishedAt` | The same moment as ISO-8601 UTC, `YYYY-MM-DDTHH:MM:SSZ`. Omitted when `created_utc` is |
| `scrapedAt` | ISO-8601 UTC stamp of when the Actor wrote the row — the Actor's clock, not Reddit's |
| `permalink` | Reddit-relative path to the post, e.g. `/r/technology/comments/…/` |
| `postUrl` | The same post as an absolute `https://www.reddit.com` link |
| `outboundUrl` | The URL the post links out to. Omitted when empty |
| `domain` | Link domain Reddit shows under the title, e.g. `reuters.com` or `self.technology`. Omitted on search-sourced rows |
| `image_url` | Image or media URL for link posts. `""` for self posts and for every search-sourced row |
| `thumbnail_url` | Thumbnail image URL, or one of Reddit's placeholder words `self`, `default`, `nsfw`, or `""` |
| `linkFlairText` | Post flair text. Omitted when the post has no flair |
| `over18` | `true` when Reddit flags the post NSFW |
| `spoiler` | `true` when the post is marked a spoiler. Always `false` on search-sourced rows |
| `comments` | Array of comment objects, nested. `[]` when `maxComments` is `0` or the thread could not be read |
| `matchedKeywords` | Array of the watchlist terms that matched this post, lowercased. `[]` when keyword filtering is off |
| `matchedKeyword` | The first entry of `matchedKeywords`, for a flat CSV column. Omitted when there was no match |
| `keywordMode` | The `filterKeywordMode` in force for this run. Omitted entirely when `filterKeywords` was empty |
| `passedFilters` | Always `true` on a charged row — posts that fail the filters are never written. An audit echo |
| `minScoreApplied` | The `minScore` value in force for this run, echoed onto every row |
| `success` | `true` on charged post rows, `false` on uncharged error rows |
| `error_message` | Only present on uncharged error rows. Omitted from every successful row |

#### Post identity, text and link fields

`title` and `body` are the two text fields worth indexing. `body` is Reddit's complete self-post text flattened to plain text — never truncated, but **empty for link posts and for every row that came from a search-term source**, because Reddit's search results page does not include post bodies. `permalink` is the relative path, `postUrl` the absolute link, and `outboundUrl` the external destination for link posts. `domain` reads `self.<subreddit>` for text posts, which is also how the Actor decides whether to populate `image_url`.

`author` and `authorFullname` are two views of the same person: the display username, which can change, and Reddit's stable `t2_` account id, which does not.

#### Engagement, safety and filter-provenance fields

`score` and `num_comments` are Reddit's own counters read straight off the page. **Reddit fuzzes vote counts and they move constantly, so `score` is a point-in-time snapshot taken at fetch — a post that read 480 when it was collected may read differently on Reddit a minute later.** Treat a `minScore` result as "cleared the floor when we looked", not as a fixed property of the post. `numCrossposts`, `over18` and `spoiler` come from the same listing attributes; on search-sourced rows `numCrossposts` is `0` and `spoiler` is `false`, because Reddit's search results markup does not carry them.

`matchedKeywords`, `matchedKeyword`, `keywordMode` and `minScoreApplied` exist so a row explains itself months later: you can see which watchlist term caught the post and what thresholds were in force, without keeping the run input alongside the data.

Each object inside `comments` has exactly five keys — `author` (falling back to `[deleted]` when Reddit renders no author link), `body`, `score`, `created_utc` and `replies`, which is another array of the same shape. Replies nest up to 9 levels; comments with no body text are skipped rather than written as blanks.

#### 🤖 Add-on: Need additional Reddit data?

**Reddit Trends Scraper By Sort, Time & Score Filter** covers the same platform from the trend side, for when you want what is rising right now rather than what mentions your keyword. **Reddit User Profile Posts And Comments Scraper With Account Age** goes the other way — take an `author` from these rows and pull that account's full posting history with karma and account age. For the same discussion research on another platform, **Quora Search Scraper** is the closest equivalent.

### How does Reddit Posts Scraper differ from the official Reddit API?

Reddit does publish a Data API, but it is a credentialed surface: you register an application, authenticate with OAuth, and your usage falls under Reddit's developer and commercial terms. This Actor reads the same public pages a logged-out visitor sees, with no registration step, and adds filtering that the API leaves to you.

| Feature | Reddit Data API | Reddit Posts Scraper |
| ----- | ----- | ----- |
| Credentials | Registered app plus OAuth credentials; commercial use is governed by Reddit's developer terms — verify against Reddit's current published API terms | ✅ None. Reads `old.reddit.com` HTML anonymously |
| Rate limiting | Enforced per credential, under terms Reddit revises — verify against Reddit's current published API terms | Not credential-based. The Actor paces itself between pages and escalates through proxies when blocked |
| Keyword and score filtering | Not provided as this Actor defines it — you fetch a listing and filter it yourself, or learn Reddit's own search syntax | ✅ Built in: `filterKeywords` with four match modes, `minScore`, `excludeNsfw` |
| Comments | A separate call per post, returned as its own tree to stitch together | ✅ Fetched automatically for each kept post and nested under `comments` |
| Upvote ratio | Present in the JSON payload | ❌ Not returned — the Actor's own source notes record it as JSON-only and therefore dropped |
| Output shape | Reddit's raw `Listing` envelope, to be unwrapped | ✅ One flat typed row per post, same keys every run |
| Where it runs | Your infrastructure, your IP addresses, your retry logic | ✅ Apify, with the proxy ladder and retries handled |

Use the official API when you need write access, authenticated endpoints, `upvote_ratio`, or a commercial arrangement Reddit has approved. Use this Actor when you want filtered, ready-to-analyse public post data without registering an application.

### How to use Reddit Posts Scraper

Reddit Posts Scraper runs on Apify. Start it from the Apify Console or call it through the Apify API. No Reddit credentials are involved at any point.

1. Open Reddit Posts Scraper on Apify and click **Try for free**
2. Fill **Subreddits, URLs or search terms** (`startUrls`) — this is the one required input. One entry per line; mix subreddit names, `r/` names, full Reddit URLs and multi-word search phrases freely
3. Optionally set **Watchlist keywords** (`filterKeywords`) and a **Keyword match mode** (`filterKeywordMode`) to keep only posts that mention your terms
4. Optionally set a **Minimum score** (`minScore`) and switch on **Exclude NSFW posts** (`excludeNsfw`) to drop low-engagement or NSFW posts before they are written
5. Set **Max matching posts per source** (`maxPosts`) and **Max comments per post** (`maxComments`) — set `maxComments` to `0` to skip comment fetching entirely and run faster
6. Click **Start**, then export as JSON, CSV or Excel, or read the dataset through the Apify API

A run submitted with an empty or missing `startUrls` does not fall back to a default subreddit. Input validation raises `startUrls is required and cannot be empty`, and the run fails immediately with zero rows written and nothing charged.

#### How to scale to bulk Reddit post extraction

`startUrls` is a list, so bulk is the normal mode — put fifty subreddits and ten search phrases in one run. Exact duplicate lines are merged before the run starts. Beyond the list, the field is forgiving about shape: a newline-separated string works, and `{ "url": "..." }` objects are accepted alongside plain strings, so output from another tool usually drops in unchanged.

**`maxPosts` is per source, not per run.** Ten sources with `maxPosts: 25` is a run that can write up to 250 rows. Sources are processed one after another, and within a source the kept posts have their comments fetched five at a time.

### What can you do with Reddit post data?

- 🔎 A **brand monitoring lead** watching for product complaints runs `r/technology` and `r/gadgets` with `filterKeywords` set to their product names and `minScore: 25`, so only complaints the community actually upvoted reach the dashboard.
- 📈 A **market researcher** sizing demand for a category collects `title`, `body` and `score` across a month of `sortOrder: "top"` and ranks themes by `score` rather than by raw post volume.
- 💼 A **recruiter** sourcing from niche communities filters the dataset on `linkFlairText` and reads `author` plus `permalink` to reach candidates in the thread where they posted.
- 🧭 A **community manager** tracking sentiment shifts pulls the nested `comments` tree, where each object's `score` shows which replies the room agreed with, not just which post started the conversation.
- 🤖 An **AI engineer** building a discussion-aware assistant indexes `title` and `body` into a vector store with `subreddit`, `score`, `publishedAt` and `matchedKeywords` as metadata filters, so the agent can answer "what are people saying about this right now" from live threads instead of a stale export.

Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.

### How does Reddit Posts Scraper handle rate limits and blocking?

Egress runs on a **proxy escalation ladder that starts at the cheapest rung**. With the default proxy setting the run begins on a **direct connection with no proxy at all**. Only when Reddit pushes back does the Actor climb: direct → Apify datacenter → Apify residential, logging each switch and staying on the higher rung once it has escalated. On the residential rung it rotates to a fresh IP up to three more times before giving up on a target.

Requests are made with `curl_cffi` impersonating Chrome's TLS fingerprint and carrying a full desktop browser header set — no headless browser is launched, which is why comment-heavy runs stay light. A response counts as blocked when it returns 403 or 429, or when it returns 200 but contains no post or comment containers, which is what Reddit serves for a private, quarantined or banned subreddit. Each page gets up to four attempts, with a wait that grows by one second per attempt and a proxy escalation between them.

There is **no CAPTCHA solving** in this Actor, and none is claimed. If a page is still blocked after every attempt the Actor logs a warning, writes nothing for that source and moves to the next one. A comment fetch that fails is caught separately: the post row is still written, with `comments` set to `[]`.

### ⬇️ Input

Twelve parameters. Only `startUrls` is required — everything else has a working default, so the minimum viable input is a single line of text.

| Parameter | Required | Type | Description | Example Value |
| ----- | ----- | ----- | ----- | ----- |
| `startUrls` | **Yes** | array | Subreddits, Reddit URLs or search terms, one per line. Mix freely: `news` or `r/technology` (subreddit), `https://www.reddit.com/r/news/` (URL), `artificial intelligence` (search). Duplicate entries are merged. | `["r/technology", "https://www.reddit.com/r/news/", "artificial intelligence"]` |
| `filterKeywords` | No | array | Words or phrases to match against each post. Case-insensitive. Leave empty to disable keyword filtering and keep every post the source returns. Default `[]`. | `["layoffs", "acquisition"]` |
| `filterKeywordMode` | No | string | How the watchlist must match. One of `any`, `all`, `titleOnly`, `exactPhrase`. Default `any`. | `all` |
| `minScore` | No | integer | Keep only posts whose score is at least this value. Minimum `0`. Default `0`, which keeps every score. | `500` |
| `excludeNsfw` | No | boolean | Drop posts Reddit flags NSFW before they are written. Default `false`. | `true` |
| `restrictToSubreddit` | No | boolean | Run search-term sources as an in-subreddit search (`restrict_sr=1`) instead of a global Reddit search. Needs `searchSubreddit` set. Default `false`. | `true` |
| `searchSubreddit` | No | string | Subreddit name used when `restrictToSubreddit` is on. Ignored when that toggle is off. Default `""`. | `technology` |
| `sortOrder` | No | string | Reddit's feed sort. One of `hot`, `new`, `top`, `rising`. Default `top`. | `new` |
| `timeFilter` | No | string | Time window. One of `hour`, `day`, `week`, `month`, `year`, `all`. Default `week`. | `day` |
| `maxPosts` | No | integer | Maximum posts to keep **per source, after filtering**. Minimum `1`, maximum `1000`. Default `10`. | `50` |
| `maxComments` | No | integer | Top-level comments to fetch for each kept post. Minimum `0`, maximum `1000`. Set `0` to skip comments. Default `5`. | `20` |
| `proxyConfiguration` | No | object | Apify Proxy configuration. Prefilled `{"useApifyProxy": false}`, which starts the run on a direct connection; the Actor escalates to datacenter and then residential on its own when blocked. | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` |

Seven honest notes on how these actually behave:

- **`filterKeywordMode` in exact terms.** Matching is plain case-insensitive substring matching, not whole-word matching — `ai` matches `said`. `any` keeps a post when at least one term appears in the title or the body. `all` requires every term to appear somewhere in title or body. `titleOnly` searches the title alone and keeps the post when **any** term matches — there is no all-terms-in-title mode. `exactPhrase` is the surprising one: it **joins your entire keyword list into a single space-separated string** and looks for that one string in title and body. `["machine", "learning"]` in `exactPhrase` searches for `machine learning`, and `matchedKeywords` returns that joined phrase rather than the individual terms, so list order matters. An empty `filterKeywords` list disables the filter entirely — every post passes, `matchedKeywords` is `[]`, and `keywordMode` is left off the row.
- **The filters run in this Actor, not on Reddit.** The Actor fetches a listing page, parses it, then applies the keyword, score and NSFW tests locally to each post. Reddit never sees your watchlist. The upside is that **discarded posts are never written and never charged** — the score floor and the keyword filter cost you nothing in row events. What they do cost is run time, because the Actor keeps paginating until it has `maxPosts` matches, so a very selective filter means many pages fetched for few rows.
- **`timeFilter` reaches Reddit differently by source type.** For subreddit and `/r/<name>` URL sources it is sent only when `sortOrder` is `top` or `rising`, exactly as the field description says. For **search-term** sources it is always sent to Reddit's search, whatever the sort.
- ⚠️ **`searchSubreddit` mangles subreddit names beginning with `r`.** The code strips leading `r` and `/` characters so it can tolerate an `r/` prefix, which correctly turns `r/technology` into `technology` — but also turns `rust` into `ust` and `relationships` into `elationships`. If your target subreddit starts with `r`, do not use `restrictToSubreddit`; put the subreddit itself in `startUrls` as its own source instead, where the name is parsed correctly.
- **`requestDelay` is accepted but is not in the schema.** The code reads a `requestDelay` value from the input and uses it as the pause in seconds between listing pages and between sources, defaulting to `1` plus up to half a second of random jitter. The Console will not show the field because it is not in the input schema; an API caller can send it. Negative values are clamped to `0`.
- **`keywords` is an undocumented alias for `filterKeywords`.** When `filterKeywords` is absent the code falls back to a `keywords` field. Either one may also be sent as a plain string, in which case it is split on newlines and commas.
- **`maxPosts` and `maxComments` are clamped by this Actor, not by Reddit.** Values are forced into 1–1000 and 0–1000 respectively. `maxComments` caps **top-level** comments only — the replies nested under those comments are returned in full, so a run with `maxComments: 5` on a busy thread can still return dozens of comment objects.

#### Example input

```json
{
  "startUrls": [
    "r/technology",
    "https://www.reddit.com/r/news/",
    "artificial intelligence"
  ],
  "filterKeywords": ["layoffs", "acquisition", "restructuring"],
  "filterKeywordMode": "any",
  "minScore": 250,
  "excludeNsfw": true,
  "restrictToSubreddit": false,
  "searchSubreddit": "",
  "sortOrder": "top",
  "timeFilter": "week",
  "maxPosts": 50,
  "maxComments": 10,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

### ⬆️ Output

Typed, normalized JSON with the same key names on every run. Rows stream into the dataset as each post is processed, so the table fills while the run is still going. Export as JSON, CSV or Excel, or read the dataset through the Apify API.

Keys whose value would be `null` are **omitted from the row rather than written as `null`**, so use a presence check (`row.get("linkFlairText")`) instead of assuming a key exists. Empty strings, `false` and `0` survive and are always written. The keys most often absent are `linkFlairText`, `domain`, `outboundUrl`, `authorFullname`, `matchedKeyword`, `keywordMode` and `error_message`.

**Error rows are pushed uncharged.** When a post fails while its comments are being fetched or its row is being built, the Actor writes a short accounting row with **no charged event attached** — it costs nothing. The verified marker is the `success` field, backed by `error_message`: charged post rows always carry `"success": true` and have `error_message` stripped out entirely, while uncharged error rows carry `"success": false` and an `error_message` string. There is no `errorReason`, `status`, `isDiagnostic`, `rowType` or `charged` field anywhere in this Actor — `success` is the one to filter on:

```python
posts = [r for r in items if r.get("success") is True]
errors = [r for r in items if r.get("success") is False]
```

An error row carries only `post_id`, `title`, `author`, `subreddit`, `success` and `error_message`. Note that it is written only when an already-collected post fails downstream; a source that could not be fetched at all, or that returned nothing passing the filters, writes **no row of any kind** and shows up only in the run log.

#### Example output

A full post row, from a subreddit source with keyword filtering on:

```json
{
  "post_id": "1n4qk2x",
  "title": "Chipmaker announces 4,000 layoffs as it restructures its foundry division",
  "author": "quietanalyst",
  "created_utc": 1753372841.0,
  "num_comments": 1842,
  "score": 24713,
  "permalink": "/r/technology/comments/1n4qk2x/chipmaker_announces_4000_layoffs_as_it/",
  "image_url": "https://www.reuters.com/technology/chipmaker-foundry-restructuring-2026-07-24/",
  "thumbnail_url": "https://b.thumbs.redditmedia.com/8Kx2fQ0pVn3yTgHc6mLwZrJd1sAoEeYb.jpg",
  "body": "",
  "comments": [
    {
      "author": "semi_engineer_",
      "body": "The foundry side has been burning cash for six quarters. This was priced in the moment they split the reporting segments.",
      "score": 3841,
      "created_utc": 1753375102.0,
      "replies": [
        {
          "author": "marketwatcher88",
          "body": "Priced in for investors, not for the 4,000 people who found out from a press release.",
          "score": 2204,
          "created_utc": 1753376890.0,
          "replies": []
        }
      ]
    },
    {
      "author": "[deleted]",
      "body": "[removed]",
      "score": 412,
      "created_utc": 1753377455.0,
      "replies": []
    }
  ],
  "subreddit": "technology",
  "success": true,
  "matchedKeywords": ["layoffs", "restructuring"],
  "matchedKeyword": "layoffs",
  "keywordMode": "any",
  "passedFilters": true,
  "minScoreApplied": 250,
  "authorFullname": "t2_9k4mzq1x",
  "subredditType": "public",
  "over18": false,
  "spoiler": false,
  "domain": "reuters.com",
  "outboundUrl": "https://www.reuters.com/technology/chipmaker-foundry-restructuring-2026-07-24/",
  "numCrossposts": 37,
  "linkFlairText": "Business",
  "publishedAt": "2026-07-24T15:20:41Z",
  "postUrl": "https://www.reddit.com/r/technology/comments/1n4qk2x/chipmaker_announces_4000_layoffs_as_it/",
  "scrapedAt": "2026-07-25T09:14:02Z"
}
```

A self post from the same run, showing `body` populated and `domain` reading `self.<subreddit>`:

```json
{
  "post_id": "1n4t8vd",
  "title": "My team was part of yesterday's layoffs — here is what the severance package actually looked like",
  "author": "throwaway_fab_2026",
  "created_utc": 1753401277.0,
  "num_comments": 596,
  "score": 8104,
  "permalink": "/r/technology/comments/1n4t8vd/my_team_was_part_of_yesterdays_layoffs_here_is/",
  "image_url": "",
  "thumbnail_url": "self",
  "body": "Twelve weeks base, prorated bonus, health cover to the end of the quarter, and equity vesting stops on the termination date rather than the notice date. That last part is the one nobody reads until it matters.",
  "comments": [],
  "subreddit": "technology",
  "success": true,
  "matchedKeywords": ["layoffs"],
  "matchedKeyword": "layoffs",
  "keywordMode": "any",
  "passedFilters": true,
  "minScoreApplied": 250,
  "authorFullname": "t2_1fq7ce3n",
  "subredditType": "public",
  "over18": false,
  "spoiler": false,
  "domain": "self.technology",
  "outboundUrl": "https://www.reddit.com/r/technology/comments/1n4t8vd/my_team_was_part_of_yesterdays_layoffs_here_is/",
  "numCrossposts": 2,
  "publishedAt": "2026-07-24T23:14:37Z",
  "postUrl": "https://www.reddit.com/r/technology/comments/1n4t8vd/my_team_was_part_of_yesterdays_layoffs_here_is/",
  "scrapedAt": "2026-07-25T09:14:19Z"
}
```

Note what is missing from that second row: no `linkFlairText`, because the post has no flair. It is absent, not `null`.

### How does it work?

Each line of `startUrls` is classified as a URL, a subreddit or a search phrase, then turned into an `old.reddit.com` address with your sort and time range attached. Requests go out over `curl_cffi` with Chrome's TLS fingerprint and a desktop header set, carrying an `over18=1` age-confirmation cookie — no browser is launched and no account is used. The returned HTML is parsed with `lxml`: subreddit listings are read from Reddit's own `data-*` post attributes, search pages from their result blocks.

Each parsed post is then tested locally against your keyword watchlist, score floor and NSFW switch, and only survivors go on to have their comment page fetched and their row written. The Actor follows Reddit's `after=` cursor page by page until it has enough matches, stopping after **40 listing pages per source** — an Actor-imposed safety cap — or when Reddit stops offering a next page. Because fields are read from Reddit's structured post attributes rather than from styling classes, a Reddit front-end restyle does not change your key names.

### Integrations

Reddit Posts Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset — your own code, scheduled runs, webhooks, or a no-code automation platform.

#### Calling Reddit Posts Scraper from Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("<YOUR_USERNAME>/reddit-posts-scraper-by-keyword-score-filter").call(run_input={
    "startUrls": ["r/technology", "artificial intelligence"],
    "filterKeywords": ["layoffs", "acquisition"],
    "filterKeywordMode": "any",
    "minScore": 250,
    "maxPosts": 50,
    "maxComments": 10,
    "proxyConfiguration": {"useApifyProxy": True},
})

for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row.get("success"):
        print(row["score"], row["subreddit"], row["matchedKeywords"], row["postUrl"])
```

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Filter on `success` to separate charged post rows from uncharged error rows.

#### No-code tools (n8n, Make, LangChain)

In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; an IF node on `score` or a Filter node on `matchedKeywords` routes hits to Slack or a database. In Make, the Apify module supports run-and-wait, so a daily keyword sweep can feed a Google Sheets or Airtable step directly. In LangChain, wrap the run endpoint as a tool and hand `title` plus `body` straight to the model, since both arrive as flat plain text. Apify schedules and webhooks handle recurrence and completion triggers, so a standing brand monitor needs no code of your own.

### Is it legal to scrape Reddit posts?

Scraping publicly accessible pages is broadly treated as permissible where no authentication is bypassed, and Reddit Posts Scraper reads only what a logged-out visitor sees — no account, no login, no session. Reddit's terms of service still govern automated access to and reuse of its site, and the post and comment text you collect is user-generated writing its authors own.

**This Actor returns personal data, and the pseudonymity does not exempt it.** Reddit usernames are handles rather than real names, but under GDPR and UK GDPR a pseudonymous identifier that consistently picks out the same individual across records is still personal data — and Reddit's identifiers are unusually stable. The fields concerned are `author` and `authorFullname` on every post row, and `author` inside every object in the `comments` tree, including nested `replies`. Post and comment text also routinely reveals political and religious beliefs, health conditions and sexuality, which are special categories under GDPR Article 9.

One factual point about scale: assembling many posts by the same `author` produces a behavioural profile — where someone posts, when, and about what — that is substantially more identifying than any single post read on its own.

Practical steps that follow from this:

- **Lawful basis.** Decide yours before you store rows. Legitimate interest is the usual route for research and monitoring, and it needs a documented balancing assessment.
- **Minimisation.** If you only need the text and the engagement, drop `author`, `authorFullname` and the `author` key inside `comments` at ingestion. Everything else — `title`, `body`, `score`, `subreddit`, `linkFlairText` — analyses fine without them.
- **Retention.** Set a deletion date. Reddit users delete posts and accounts, and a dataset that outlives its source drifts out of accuracy as well as out of compliance.
- **Subject requests.** Keep `post_id` and `permalink` so you can locate and delete a specific person's records when asked. Under CCPA the same rows are personal information about California residents.

Consult legal counsel if your use case involves bulk storage of personal data, or republication of collected post and comment text.

### ❓ Frequently asked questions

#### What Reddit post fields does Reddit Posts Scraper return?

The five most used are `title`, `body`, `score`, `author` and `comments`. Every charged row also carries `subreddit`, `permalink`, `postUrl`, `publishedAt`, `num_comments`, `linkFlairText`, `over18` and the filter-provenance fields `matchedKeywords`, `keywordMode` and `minScoreApplied` — 30 keys in total, plus 5 more inside each nested comment object. See the data fields table above for the full list.

#### Does Reddit Posts Scraper require a Reddit account, login or API key?

No. There is no OAuth flow, no registered app, no client id and no password anywhere in the Actor. It reads `old.reddit.com` pages anonymously. The one cookie it sends is `over18=1`, the age confirmation any visitor clicks past to view NSFW content — it carries no identity. The only credential involved is your Apify token, and only if you call the Actor through the API rather than the Console.

#### Are the keyword and score filters applied by Reddit, and am I charged for posts they reject?

They are applied by this Actor, after the page is fetched — and **no, rejected posts are never charged.** The Actor requests a listing page, parses every post on it, then tests each one against `filterKeywords`, `minScore` and `excludeNsfw` in its own code. Posts that fail are dropped in memory: they are never written to the dataset, so no charged event fires for them. What a selective filter does cost is run time, because the Actor keeps requesting the next page until it has `maxPosts` matches or hits the page cap.

#### How many Reddit posts can I extract in one run?

`maxPosts` accepts 1 to 1000 and applies **per source**, counted after filtering — twenty sources at `maxPosts: 100` is a run that can write up to 2,000 rows. Those bounds are this Actor's own clamps. Whether you reach the number depends on the source: the Actor follows Reddit's `after=` cursor through at most **40 listing pages per source** before stopping, and it also stops early when Reddit offers no next page. `maxComments` (0–1000) is a separate per-post budget covering top-level comments only.

#### What happens if a subreddit returns no posts that pass the filters, or is private, quarantined or banned?

Both cases end the same way for your dataset: **zero rows and zero charge for that source**, with the detail in the run log rather than in the data.

If the subreddit loads but nothing clears your filters, the Actor pages through it, logs `0 posts matched filters for source: <name>` and moves on. If the subreddit is private, quarantined or banned, Reddit returns a page containing no post containers — the Actor treats that as a block, climbs the proxy ladder, retries up to four times, then logs `No HTML returned` and moves on. Neither case writes an error row, because no post was ever collected to write one about. If the whole run ends with nothing, the log says `0 posts pushed — check filters, source validity, or proxy availability`. In code, treat an unreachable source as an absence of rows and check the log, not the dataset.

#### Can I scrape multiple subreddits and search terms at once?

Yes. `startUrls` is a list and mixes types freely — subreddit names, `r/` names, full Reddit URLs and search phrases in the same run, processed one source after another. Identical lines are merged before the run starts. One caveat on duplicates: **posts are deduplicated within a source, not across sources.** The Actor tracks post ids per source, so if `r/technology` and the search phrase `chip layoffs` both surface the same post, it is written and charged twice. Deduplicate on `post_id` after the run if that matters.

#### Does Reddit Posts Scraper work with Claude, ChatGPT and other AI agent tools?

Yes. It is callable as a standard HTTP-triggered run through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. Give the agent `title`, `body` and `comments` as the content and `score`, `subreddit` and `publishedAt` as the metadata it can reason over.

#### How does Reddit Posts Scraper compare to other Reddit post scrapers?

Checked on the Apify Store on 25 July 2026: `mysteriousshadow/detailed-reddit-posts-scraper-with-flair-filtering` documents flair-based filtering over a single subreddit URL with automated scrolling, and its documented example output uses hyphenated keys (`number-of-upvotes`, `number-of-comments`, `post-link`) with numeric values as strings; it documents no keyword filter, no score filter and no comment bodies. `maged120/reddit-subreddit` documents a `subreddits` array with `sort`, `time` and `maxPosts`, an output including `upvoteRatio` and `isSelf`, and states plainly that no Reddit API key is needed; it documents no keyword or score filtering and no comment text. `scrapers_lat/reddit-posts-scraper` documents three input modes — subreddit feeds, search query and post URLs — 26 fields per post including gallery images and awards, a global `maxItems` budget, and that a post which fails to load is written as a record with an `error` field.

What this Actor documents explicitly against that field: a keyword watchlist with four match modes plus a score floor and NSFW exclusion, all applied before any row is written or charged; nested comment trees with replies; per-row filter provenance (`matchedKeywords`, `keywordMode`, `minScoreApplied`); and uncharged error rows marked by `success: false`. It is equally explicit about what it does not return — no upvote ratio, no awards, no gallery image arrays.

#### Does Reddit Posts Scraper return data in a format LLMs can use directly?

Yes. Typed, normalized JSON with consistent key names across runs, post bodies and comment bodies already flattened from Reddit's markup to plain text, and counters already coerced to integers. No HTML parsing, no selectors. Pass a row directly into an LLM, index it into a vector store, or hand it to an agent tool — just remember that keys with no value are omitted rather than set to `null`.

#### What happens when Reddit changes its layout or anti-bot system?

The scraper is maintained, and the output schema is designed to survive front-end change: posts are read from Reddit's own `data-*` attributes on the listing markup rather than from styling classes, so a restyle degrades at most one field instead of breaking the row. Every field extractor also has a fallback — a missing title becomes `"No Title"`, a missing author becomes `"Unknown"`, a missing flair is simply left off — so a partial page still produces a usable row. Your field names and types stay the same on your end.

#### Can I use Reddit Posts Scraper without managing proxies or browser infrastructure?

Yes. The TLS impersonation, the retries and the proxy ladder are all handled for you — direct, then Apify datacenter, then Apify residential with three extra IP rotations, sticky once escalated. You never create a proxy account, rotate an IP or run a browser. Leaving `proxyConfiguration` at its prefilled `{"useApifyProxy": false}` still gets you the full ladder; it only decides which rung the run starts on. For large runs, starting on Apify residential is the recommended setting. The Actor does not solve CAPTCHAs, and it makes no claim to.

#### Which Reddit fields work best for AI training data and RAG indexing?

For RAG indexing: `title` and `body` carry the most information per record and chunk cleanly, with the `body` text inside each `comments` object adding the community's response to the same topic; use `subreddit`, `linkFlairText` and `matchedKeywords` as metadata filters and `postUrl` as the citation. For training data: `score`, `num_comments`, `numCrossposts`, `over18`, `spoiler` and `publishedAt` are the most structurally consistent fields across records. All values return as typed primitives or plain arrays, so no normalization pass is needed — just account for `body` being `""` on link posts and on every search-sourced row, and for `score` being a fuzzed, point-in-time number rather than a fixed property of the post.

### 🔗 Related scrapers

| Scraper Name | What it extracts |
| ----- | ----- |
| Reddit Trends Scraper By Sort, Time & Score Filter | Trending Reddit posts by sort mode, time window and score |
| Reddit User Profile Posts And Comments Scraper With Account Age | A Reddit user's posts and comments with account age and karma stats |
| Quora Search Scraper | Quora questions, answers, upvotes and author credentials |
| Google News Scraper By Publisher & Site Filter | News articles filtered by publisher and site |
| X (Twitter) Posts Search | Public posts matching a search query |
| Threads User Posts Scraper By Keyword & Date Filter | Threads posts for a user, filtered by keyword and date |

### 💬 Your feedback

Found a bug, or need a field that is on the Reddit page but not in the output? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and the subreddit or search phrase you ran are the fastest to reproduce and fix.

# Actor input Schema

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

Mix any of:
• 📌 Subreddit — `news` or `r/technology`
• 🌐 Full URL — `https://www.reddit.com/r/news/`
• 🔍 Search term — `artificial intelligence` (runs a Reddit search)

Duplicate entries are merged. At least one line is required.

## `filterKeywords` (type: `array`):

Words/phrases to match against each post. Matching is case-insensitive. Example: `["layoffs", "acquisition"]` with mode Any keeps posts mentioning either. Leave empty to disable keyword filtering.

## `filterKeywordMode` (type: `string`):

How the watchlist keywords must match. Any = at least one term (title + body). All = every term must appear. Title only = match the title text only. Exact phrase = the terms must appear together as one phrase.

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

Only keep posts whose upvote score is at least this value. Example: `500` skips everything under 500 upvotes. Set `0` to keep all scores.

## `excludeNsfw` (type: `boolean`):

When on, posts flagged NSFW (over\_18) are dropped and never charged.

## `restrictToSubreddit` (type: `boolean`):

When on (and a subreddit is set below), any search-term source is run as an in-subreddit search (restrict\_sr=1) instead of a global Reddit search.

## `searchSubreddit` (type: `string`):

Subreddit name used when 'Search inside one subreddit only' is on. Example: `technology`. Ignored when that toggle is off.

## `sortOrder` (type: `string`):

How Reddit sorts the feed. Hot = trending now, New = latest, Top = most upvoted, Rising = gaining traction.

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

Time window for results. ⚠️ Applies only when Sort order is Top or Rising; ignored for Hot and New.

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

Maximum posts to keep per source AFTER filtering (1–1000). The actor paginates until it has this many matches or runs out of posts.

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

Comments to fetch for each kept post (0–1000). Set 0 to skip comments and only collect post metadata (faster).

## `proxyConfiguration` (type: `object`):

Proxy for the requests. If Reddit blocks a request the actor auto-escalates: none → datacenter → residential. ✅ Residential recommended for large runs.

## Actor input object example

```json
{
  "startUrls": [
    "news",
    "r/technology",
    "artificial intelligence"
  ],
  "filterKeywords": [],
  "filterKeywordMode": "any",
  "minScore": 0,
  "excludeNsfw": false,
  "restrictToSubreddit": false,
  "sortOrder": "top",
  "timeFilter": "week",
  "maxPosts": 10,
  "maxComments": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# 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 = {
    "startUrls": [
        "https://www.reddit.com/r/news/",
        "news",
        "artificial intelligence"
    ],
    "filterKeywords": [],
    "searchSubreddit": "",
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

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

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = {
    "startUrls": [
        "https://www.reddit.com/r/news/",
        "news",
        "artificial intelligence",
    ],
    "filterKeywords": [],
    "searchSubreddit": "",
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/reddit-posts-scraper").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://www.reddit.com/r/news/",
    "news",
    "artificial intelligence"
  ],
  "filterKeywords": [],
  "searchSubreddit": "",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call scraper-engine/reddit-posts-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=scraper-engine/reddit-posts-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/r7JPUiNBuGvIzWRxk/builds/B6Y2F5b8SwteLrjJU/openapi.json
