# Reddit Scraper: Subreddit Posts, Comments, Search - No Login (`santhej/reddit-scraper`) Actor

Extract Reddit posts, comments and search results at scale. Up to 1,000 posts per subreddit across hot/new/top/rising, full comment threads, and keyword search. Flat JSON, 33 always-present fields, agent-ready. No login or credentials. SFW only. No start fee, $0.60 per 1,000 posts.

- **URL**: https://apify.com/santhej/reddit-scraper.md
- **Developed by:** [Santhej Kallada](https://apify.com/santhej) (community)
- **Categories:** Lead generation, Automation, Social media
- **Stats:** 4 total users, 4 monthly users, 88.9% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

Pay per event

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

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

## What's an Apify Actor?

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

## How to integrate an Actor?

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

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

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

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

# README

## Reddit Scraper: Subreddit Posts, Comments, Search — No Login

Extract **posts, full comment threads and keyword search results** from public Reddit communities and get
back one flat dataset with **33 keys on every row**. No login, no API keys, no tokens, no OAuth app — pass a
community name and get data.

**There is no start fee.** You pay for rows, and only for rows that were actually written to your dataset.
A run that returns nothing costs **$0.00**.

- **$1.00 per 1,000 posts** (search results bill on the same event)
- **$0.30 per 1,000 comments**
- **$0.002 per comment thread** that actually returned a comment
- **$0.00 to start a run**

***

### Why the missing start fee matters

Most scrapers in this category charge a flat fee the moment a run begins, before a single row exists — and on
this platform that fee scales with the memory a run reserves, so an Actor that reserves several gigabytes
pays it several times over. For an agent making small, frequent calls, that fixed fee *is* the bill.

**A 25-post call, end to end:**

| | This Actor | Elsewhere in the category |
|---|---|---|
| Start fee | **$0.00** | $0.02 – $0.09 **before any data is returned** |
| 25 posts | $0.025 | plus $0.0125 – $0.125 |
| **Total** | **$0.025** | **$0.033 – $0.215** |
| A run that returns 0 rows | **$0.00** | the start fee, every time |

That is **1.3× to 8.6× cheaper on exactly the small, repeated calls agents make all day**, and the gap is
widest where it hurts most: retries and empty results are free here.

Unnamed price bands across the category (no developer is named — compare for yourself in the Store):

| Axis | This Actor | Published band elsewhere |
|---|---|---|
| Start fee per run | **$0.00** | $0.02 – $0.09 |
| Per 1,000 posts | **$1.00** | $0.50 – $4.99 |
| Per 1,000 comments | **$0.30** | typically the same rate as posts |
| Billed for promoted/duplicate rows | **No** | commonly yes |
| Billed when a run returns nothing | **No** | start fee applies |

At very large volumes the per-post rate here sits at the **low end** of the published band rather than far
below it; the decisive advantage is on small and mid-sized calls, and on the fact that failures and empty
results never appear on your invoice.

***

### What you get

- **Community listings** — up to **1,000 posts** per community, across `hot`, `new`, `top`, `rising`,
  `controversial` and `best`, with an optional time window.
- **Comment threads** — up to **500 comments** per post, with nesting depth, parent links and author flair.
- **Keyword search** — site-wide or scoped to specific communities, up to **~239 results** per keyword.
- **One flat dataset** — posts and comments in the same table, distinguished by `type` and `sourceType`, so a
  multi-step research task is one call instead of three Actors stitched together.
- **Deterministic shape** — all 33 keys present on every row. Inapplicable values are `null`, never a missing
  key, so a parser written once never breaks.

#### Published ceilings (enforced in the input schema, not discovered at runtime)

| Surface | Hard ceiling |
|---|---|
| Posts per community listing | **1,000** |
| Results per search keyword | **~239** (measured 100 + 100 + 39, then the listing ends) |
| Comments per post | **500** |

Asking for more does not fail — it returns the ceiling. `maxCommentsPerPost` is an **upper bound, not a
guarantee**: a request for 50 comments commonly returns 48–49 on deep threads, and very large threads are
sampled rather than exhausted (a 7,000-comment thread returns 500 across roughly 6 levels of nesting). This
is stated up front so a planning agent never has to run a trial job.

#### SFW communities only — by design

Adult and quarantined communities are **refused**, not filtered afterwards:

1. Every requested community is checked **before** any harvesting starts. Adult-gated, quarantined, banned
   and non-existent communities are rejected with a clear input-validation error and cost nothing.
2. The primary data path is the one that honours Reddit's own age gate, so the default route *cannot* return
   adult content.
3. Any individual post marked adult is dropped before it is written and before it is billed — even inside a
   SFW community.
4. Search never opts into adult results, and comment threads are only requested for posts that already
   passed.

There is **no toggle to switch this off**. This is a deliberate safety property of the product.

***

### Use cases

- **Market and product research** — track what a community says about a product, a release or a competitor.
- **Trend and topic monitoring** — schedule a daily `new` pull on a set of communities and diff it.
- **Sentiment and NLP corpora** — post bodies plus full comment threads, already flat and deduplicated.
- **Support and bug triage** — find complaint threads about your product across relevant communities.
- **Lead and community discovery** — find the communities where a topic is actually discussed.
- **Agent workflows** — a small, cheap, predictable call an LLM agent can make dozens of times per task.

***

### Input

Nine fields, eight of them optional. Supply **communities**, a **search keyword**, or both — a run with
neither fails immediately at validation and costs nothing.

| Field | Type | Default | Notes |
|---|---|---|---|
| `subreddits` | array | `["programming"]` | Accepts `programming`, `r/programming`, `/r/programming` or a full reddit.com URL. |
| `searchQuery` | string | — | Site-wide on its own. With `subreddits`, runs once per community **in addition to** each community's listing — see below. |
| `sort` | string | `hot` | `hot`, `new`, `top`, `rising`, `controversial`, `best`. |
| `timeRange` | string | `all` | `hour`, `day`, `week`, `month`, `year`, `all`. Applies to `top`, `controversial` and search. |
| `maxPostsPerSource` | integer | `100` | Max 1,000. Per **source**, not per run — see below. |
| `includeComments` | boolean | `false` | Appends comment rows to the same dataset. |
| `maxCommentsPerPost` | integer | `50` | Max 500. Upper bound, not a guarantee. |
| `minCommentsToFetch` | integer | `1` | Skip threads on posts declaring fewer comments than this. Your direct lever on the per-thread charge. |
| `proxyConfiguration` | object | Residential US | Advanced. |

#### What counts as a source

`maxPostsPerSource` is a ceiling **per source**, and a run can have more sources than it has
communities. A source is one community listing, one keyword search inside one community, or one
site-wide search. So the row count — and the bill — is:

| Input | Sources | Rows at `maxPostsPerSource: 8` |
|---|---|---|
| 1 community | 1 listing | 8 |
| keyword only | 1 site-wide search | 8 |
| 1 community + keyword | 1 listing + 1 search | 16 |
| 2 communities + keyword | 2 listings + 2 searches | **32** |

Supplying communities **and** a keyword therefore returns roughly twice what either does alone: you
get each community's current listing *and* that community's matches for your keyword. If you only
want keyword matches, leave `subreddits` empty and let the search run site-wide, or pass the keyword
alone and filter afterwards. Every row states its origin in `sourceType` (`subreddit` or `search`)
and `sourceQuery`, so the two are always separable, and overlap between a listing and a search of the
same community is de-duplicated before billing.

#### Minimal input

```json
{
  "subreddits": ["programming"]
}
```

Returns the 100 hot posts of `r/programming` for **$0.10**.

#### Full input

```json
{
  "subreddits": ["programming", "r/rust", "https://www.reddit.com/r/webdev/"],
  "searchQuery": "state management",
  "sort": "top",
  "timeRange": "month",
  "maxPostsPerSource": 250,
  "includeComments": true,
  "maxCommentsPerPost": 100,
  "minCommentsToFetch": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

#### Search only (site-wide)

```json
{
  "searchQuery": "rust async runtime",
  "sort": "new",
  "timeRange": "year"
}
```

***

### Output

One dataset. One record shape. Every key present on every row.

#### Post row

```json
{
  "type": "post",
  "id": "t3_1abc234",
  "postId": "t3_1abc234",
  "parentId": null,
  "depth": null,
  "subreddit": "programming",
  "subredditPrefixed": "r/programming",
  "subredditId": "t5_2fwo",
  "author": "example_user",
  "authorId": "t2_9xk21f",
  "title": "A deep dive into structured concurrency",
  "body": null,
  "url": "https://example.com/structured-concurrency",
  "permalink": "https://www.reddit.com/r/programming/comments/1abc234/a_deep_dive_into_structured_concurrency/",
  "domain": "example.com",
  "postType": "link",
  "score": 1284,
  "upvoteRatio": null,
  "numComments": 213,
  "numCrossposts": 2,
  "awardCount": 0,
  "flair": "Discussion",
  "isNsfw": false,
  "isSpoiler": false,
  "isOriginalContent": false,
  "isPinned": false,
  "createdAt": "2026-08-14T14:25:48.264Z",
  "createdTimestamp": 1786717548264,
  "rank": 1,
  "sourceType": "subreddit",
  "sourceQuery": "programming",
  "sortUsed": "hot",
  "scrapedAt": "2026-08-14T16:41:30.117Z"
}
```

#### Comment row

```json
{
  "type": "comment",
  "id": "t1_kx99f0a",
  "postId": "t3_1abc234",
  "parentId": null,
  "depth": 0,
  "subreddit": "programming",
  "subredditPrefixed": "r/programming",
  "subredditId": "t5_2fwo",
  "author": "another_user",
  "authorId": "t2_4b7zq2",
  "title": null,
  "body": "The cancellation semantics are the part people underestimate.",
  "url": null,
  "permalink": "https://www.reddit.com/r/programming/comments/1abc234/a_deep_dive_into_structured_concurrency/kx99f0a/",
  "domain": null,
  "postType": null,
  "score": 42,
  "upvoteRatio": null,
  "numComments": null,
  "numCrossposts": null,
  "awardCount": 0,
  "flair": null,
  "isNsfw": null,
  "isSpoiler": null,
  "isOriginalContent": null,
  "isPinned": null,
  "createdAt": "2026-08-14T15:58:02.000Z",
  "createdTimestamp": 1786723082000,
  "rank": 1,
  "sourceType": "comments",
  "sourceQuery": "programming",
  "sortUsed": "hot",
  "scrapedAt": "2026-08-14T16:41:30.117Z"
}
```

#### Field reference

| # | Field | Type | Description |
|---|---|---|---|
| 1 | `type` | string | `post` or `comment`. |
| 2 | `id` | string | `t3_` for posts, `t1_` for comments. Primary dedupe key. |
| 3 | `postId` | string | `t3_` id of the parent post; equals `id` on post rows. |
| 4 | `parentId` | string | Direct parent id. `null` on posts and top-level comments. |
| 5 | `depth` | integer | Comment nesting depth, 0 = top level. `null` on posts. |
| 6 | `subreddit` | string | Bare community name. |
| 7 | `subredditPrefixed` | string | `r/`-prefixed name. |
| 8 | `subredditId` | string | `t5_` id. |
| 9 | `author` | string | Username without `u/`. `[deleted]` is preserved verbatim. |
| 10 | `authorId` | string | `t2_` id. |
| 11 | `title` | string | Post title, HTML entities decoded. `null` on comments. |
| 12 | `body` | string | Self-text, comment text, or search snippet. |
| 13 | `url` | string | Outbound/media URL. Equals the permalink for text posts. |
| 14 | `permalink` | string | Absolute link to the post or the specific comment. |
| 15 | `domain` | string | Link host, or `self.{community}` for text posts. |
| 16 | `postType` | string | `link`, `text`, `image`, `video`, `gallery`, `crosspost`. |
| 17 | `score` | integer | Net upvotes at extraction time. `null` when hidden. |
| 18 | `upvoteRatio` | number | 0–1. See the availability matrix below. |
| 19 | `numComments` | integer | Declared comment count on the post. |
| 20 | `numCrossposts` | integer | Crosspost count. |
| 21 | `awardCount` | integer | Award count. |
| 22 | `flair` | string | Post flair, or user flair on comment rows. |
| 23 | `isNsfw` | boolean | Always `false` on delivered post rows — adult rows are never written. |
| 24 | `isSpoiler` | boolean | Spoiler marker. |
| 25 | `isOriginalContent` | boolean | OC marker. |
| 26 | `isPinned` | boolean | Pinned/stickied in the community. |
| 27 | `createdAt` | string | ISO 8601 UTC. |
| 28 | `createdTimestamp` | integer | Epoch **milliseconds**, derived from `createdAt` so the two can never disagree. |
| 29 | `rank` | integer | 1-based position within its own listing, search or thread. |
| 30 | `sourceType` | string | `subreddit`, `search` or `comments`. |
| 31 | `sourceQuery` | string | The community or keyword that produced the row. |
| 32 | `sortUsed` | string | The sort actually applied, echoed back. |
| 33 | `scrapedAt` | string | ISO 8601 UTC extraction time, identical for every row in a run. |

#### Field availability matrix

The record **shape** is identical everywhere; the **populated set** is not, and pretending otherwise would
silently break anything filtering on a field it cannot tell apart from unknown. So it is published — and it is
published **per route**, because every one of the three surfaces has a primary route and a fallback route, and
the fallback route reports strictly less. The fallback is only used when the primary route is unavailable for
that source.

`+` populated · `–` always `null` · `0` always `false` by construction

| Field | Listing (primary) | Listing (fallback) | Search (primary) | Search (fallback) | Comments (primary) | Comments (fallback) |
|---|:--:|:--:|:--:|:--:|:--:|:--:|
| `type` `id` `postId` `permalink` `rank` `sourceType` `sourceQuery` `sortUsed` `scrapedAt` | + | + | + | + | + | + |
| `parentId` `depth` | – | – | – | – | + | + |
| `subreddit` `subredditPrefixed` `author` `score` `createdAt` `createdTimestamp` | + | + | + | + | + | + |
| `title` | + | + | + | + | – | – |
| `subredditId` | + | + | – | – | + | **–** |
| `authorId` | + | + | + | + | + | **–** |
| `body` | + | + | + (snippet) | + (snippet) | + | + |
| `url` `domain` | + | + | **+** | **–** | – | – |
| `postType` | + | + | – | – | – | – |
| `upvoteRatio` | – | + | – | – | – | – |
| `numComments` | + | + | + | + | – | – |
| `numCrossposts` | + | – | – | – | – | – |
| `awardCount` | + | + | – | – | + | + |
| `flair` | + | + | **+** | **–** | + (user flair) | **–** |
| `isNsfw` | 0 | – | – | – | – | – |
| `isSpoiler` `isOriginalContent` `isPinned` | + | – | – | – | – | – |

**Exactly what a fallback route costs you**, so it is never discovered as a surprise null column:

| Surface | Fields the fallback route cannot report that the primary one can |
|---|---|
| Community listing | `numCrossposts`, `isNsfw`, `isSpoiler`, `isOriginalContent`, `isPinned` (it gains `upvoteRatio`) |
| Search | `url`, `domain`, `flair` |
| Comments | `subredditId`, `authorId`, `flair` |

The primary listing route populates **32 of 33 fields** (only `upvoteRatio` is unavailable there); the fallback
listing route populates 26 of 33. If you need `upvoteRatio` on every row, this Actor is not the right tool —
that is said here rather than discovered as a null column.

#### Which route you actually get

The **primary route is the route** — the fallback is not a route most runs touch. Measured on hosted runs
against the shipped residential configuration: runs complete on the primary route, at 100 posts per request,
and reach the fallback only when the site refuses several consecutive exit addresses in a row for one source.

This is worth stating plainly because it is where the field matrix above becomes a promise rather than a
table. Reddit refuses a share of anonymous requests per exit address — it is a property of the address, not of
your query, your community or your sort — so the Actor simply takes another address and asks again, up to six
times per source, before it gives up on the route. That is the whole mechanism, and it is why the primary
route's 32 of 33 fields is the set you should plan against.

Two consequences you can rely on:

- **A refused address is never delivered as an empty result.** A page carrying no rows is only accepted as
  "this source is empty" when it is demonstrably a page the surface itself served — an empty community renders
  its listing container, a refusal does not. Anything else advances the route, and if no route can answer the
  run **fails**, with a message naming the source. It never reports a short or empty dataset as a complete one.
- **A genuinely empty source is still a normal success.** A community with no posts, or a keyword with no
  matches, returns zero rows, costs $0.00, and is counted in the run summary's `emptySources` — which is what
  separates it from a failure.

Search rows carry the post's own link target (`url`, and `domain` derived from it — `self.{community}` for a
text post), which is more than a search row usually returns; `postType` is not among them, because this
surface does not state the post's shape and it is not guessed. `body` on a search row is the **snippet**, not
the post text, and `sourceType: "search"` marks every such row so a parser can branch on it.

***

### Pricing

Pay per event. **No start fee.**

| Event | Price | When it fires |
|---|---|---|
| Post scraped | **$0.001** ($1.00 / 1,000) | Per post row written to the dataset. Search results bill on this same event. |
| Comment scraped | **$0.0003** ($0.30 / 1,000) | Per comment row written to the dataset. |
| Comment thread fetched | **$0.002** | Once per post whose thread returned at least one comment. Empty and deleted threads are free. |

**Cost formula you can compute before calling:**

```
total = 0.001 * posts + 0.002 * threadsFetched + 0.0003 * comments
```

**Worked examples**

| Job | Cost |
|---|---|
| 25 posts (typical agent call) | **$0.025** |
| 100 hot posts from one community | **$0.10** |
| 1,000 posts from one community | **$1.00** |
| 3 communities × 500 posts | **$1.50** |
| 100 posts + threads (100 threads, 25 comments each) | **$1.05** |
| 1,000 posts + threads (700 threads, 8 comments each) | **$4.08** |
| A run that returns no rows | **$0.00** |

**Billing rules**

1. Charges fire **only after** rows are verified and successfully written. A failed or empty run is free.
2. Promoted rows and cross-source duplicates are dropped **before** they are written, so you are never billed
   for them. The count you are charged equals the count of usable rows.
3. `minCommentsToFetch` is a direct lever on the per-thread charge — raise it to skip low-value threads.
4. `maxPostsPerSource` and `maxCommentsPerPost` bound the row events, so your maximum bill is knowable in
   advance.

***

### Notes on reliability

The Actor manages its own request rate and retries transient failures. Known silent-failure modes are turned
into loud errors: if a source cannot be delivered, the run fails with a message you can act on instead of
returning a short dataset you would treat as complete. Runs are capped by the ceilings above so a request can
never quietly under-deliver against an optimistic limit.

**What "loud" means here, concretely.** These are the rules, not aspirations:

| Situation | What you get |
|---|---|
| A source really has nothing in it | `SUCCEEDED`, zero rows, `emptySources` incremented, **$0.00** |
| A page comes back that is not the surface at all | the route is retried on a fresh exit address, then advanced |
| No route can answer a source | that source is reported in `failedSources` and `partial: true` |
| Some sources collected, others failed | `SUCCEEDED` with `partial: true` — read it before treating the dataset as complete |
| Nothing collected at all, and no source was empty | the run **fails** — status `FAILED`, non-zero exit, a message naming the cause, and the summary still written to `OUTPUT` |

A run that fails reports `FAILED`. It does not report success with an empty dataset, and it does not charge
you — billing fires only after rows are written.

**The run summary** is written to the key-value store as `OUTPUT` and includes `posts`, `rows`, `tierUsed` per
surface, `failedSources`, `emptySources`, `partial`, `challengesEncountered`, `requiredFieldFill` per route,
and `estimatedChargeUsd`. Read `partial` and `failedSources` before treating a dataset as complete — they are
there so you never have to infer completeness from a row count.

#### Run memory: 2 GB, fixed

This Actor runs at **2048 MB**, declared in its own definition (`minMemoryMbytes`, `defaultMemoryMbytes` and
`maxMemoryMbytes` are all 2048), so the figure is not a default you can drift off:

- **2 GB is measured, not padded.** The extraction path renders no page: the worst case is ~1.15 GB with the
  fallback surface active and the primary one still alive. Most Actors in this category ship at 4 GB, which
  doubles the compute line for the same rows.
- **Do not lower it.** At 1024 MB a large run is OOM-killed by the platform rather than degraded, and on this
  platform an OOM kill is a failed run, not a slow one.
- **Do not raise it.** Nothing here uses the extra memory, and compute is billed per GB-hour, so 4 GB simply
  doubles that part of your bill.

If you run this Actor from the API or a scheduler and pass explicit run options, leave `memoryMbytes` unset —
the declared default applies. Setting it anyway is the only way to end up outside the tested configuration.

***

### Data use, privacy and compliance — please read

Output contains **personal data** (usernames, user ids, authored text, timestamps, community membership).

1. **You are the data controller.** You are responsible for establishing a lawful basis (GDPR Art. 6),
   providing notice where required (Art. 14), and setting a retention period. This Actor is a processing tool;
   it does not make those determinations for you.
2. **No profiling of individuals.** This Actor must not be used to build person-level profiles or datasets
   about identified or identifiable individuals. It has no user-profile surface and will not get one.
3. **Honour deletions.** Content deleted or removed on Reddit after extraction must stop being used. Re-verify
   before reusing an older dataset.
4. **Special-category warning (GDPR Art. 9).** Membership of a community can itself reveal health status,
   sexual orientation, religious belief or political opinion. Harvesting such communities may constitute
   Art. 9 processing that you cannot lawfully perform. The "manifestly made public" exception is assessed
   **per item** and cannot be applied wholesale by a bulk collector.
5. **Adult and quarantined communities are refused**, with no opt-in (see above).
6. Respect Reddit's terms and the rights of the people whose content you collect. If you are unsure whether
   your intended use is lawful in your jurisdiction, take advice before running at scale.

***

### FAQ

**Do I need a Reddit account, an app, or API keys?**
No. There is nothing to configure beyond the input fields.

**What does a run that returns nothing cost?**
Nothing. There is no start fee and charges only fire on rows that were written.

**I got zero rows and the run says SUCCEEDED. Is that real?**
Yes — and it is the only case in which you will ever see it. Zero rows plus `SUCCEEDED` means the source
genuinely had nothing in it, and the run summary says so explicitly in `emptySources`. If the Actor could not
collect a source, the run does not succeed: the source appears in `failedSources` with `partial: true`, or the
whole run reports `FAILED` with a message naming the cause. A short or empty dataset is never presented to you
as a complete one.

**How many posts can I get from one community?**
1,000 per listing. That is a hard platform ceiling, not a limitation of this Actor. To go further, run again
with a different `sort` — cross-sort overlap is typically 30–60%, and deduplicating on `id` is trivial since
`id` is stable.

**Why does my search return about 240 results and stop?**
That is the ceiling for a single keyword. Split a broad topic into several narrower keywords, or scope the
search to specific communities and run one call per community.

**Why did I get 48 comments when I asked for 50?**
`maxCommentsPerPost` is an upper bound. Deleted, collapsed and deeply nested replies reduce the delivered
count. Very large threads are sampled, not exhausted.

**Can I scrape adult or quarantined communities?**
No. They are refused before any data is fetched, and there is no setting to change that.

**Can I get comments without posts?**
Comments are always attached to the posts they belong to — set `includeComments: true` and filter the dataset
on `type == "comment"`. Both live in the same table, joined by `postId`.

**Can I export to CSV or Excel?**
Yes — the dataset exports to JSON, CSV, XLSX, XML and HTML from the run page or the API, and the flat, fixed
33-key shape means the columns are always the same.

**Can an AI agent call this?**
Yes, that is the design target. Nine input fields, one required decision, published ceilings, a computable
cost formula, no pagination state and no cursors to manage. One call in, complete dataset out.

**Is the output shape stable?**
Yes. All 33 keys appear on every row, in the same order, with `null` for inapplicable values. New fields, if
any, would be added at the end.

***

### Tags

`reddit` · `reddit scraper` · `subreddit scraper` · `reddit comments` · `reddit search` · `social media` ·
`sentiment analysis` · `market research` · `nlp dataset` · `ai agent` · `mcp` · `no login`

# Actor input Schema

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

Communities to harvest, one per line. Accepts 'programming', 'r/programming', '/r/programming' or a full reddit.com URL — all are normalised to the bare name. Supply this, a search keyword, or both. Adult and quarantined communities are refused.

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

Keyword search. On its own it runs site-wide and returns posts from any community. If communities are also supplied, the search runs once PER community — and the plain listing for each community still runs as well, so communities + a keyword is twice as many sources, and twice as many billable rows, as either on its own. Every row records which it came from in `sourceType` (`subreddit` or `search`) and `sourceQuery`. A single keyword returns roughly 240 results at most — split broad topics into several keywords.

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

Listing order. 'best' is served as 'hot' and the row field sortUsed echoes what actually ran. For a keyword search these map to relevance / top / new / most comments.

## `timeRange` (type: `string`):

Only meaningful for Top, Controversial and keyword search. It widens the history window but does not raise the 1,000-post ceiling — the windows are nested subsets of each other.

## `maxPostsPerSource` (type: `integer`):

Posts to return per SOURCE, not per run. A source is one community listing, or one keyword search within one community, or one site-wide search — so 2 communities plus a keyword is 4 sources and returns up to 4× this number. 1,000 is the hard ceiling per source — a community listing stops delivering beyond that no matter what is requested.

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

Fetch the comment thread of every harvested post and append the comments to the same dataset as rows with type = 'comment'. Off by default so a plain listing run stays cheap.

## `maxCommentsPerPost` (type: `integer`):

Upper bound per post, not a guarantee — 500 is the hard ceiling and a request for 50 typically returns slightly fewer on deep threads. Ignored when comment threads are switched off.

## `minCommentsToFetch` (type: `integer`):

Skip the thread request entirely for posts whose declared comment count is below this number. At the default of 1, posts with zero comments cost nothing. This is the direct lever on the per-thread charge.

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

Advanced. Residential US is the shipped default and is what the published reliability figures are based on.

## Actor input object example

```json
{
  "subreddits": [
    "programming"
  ],
  "sort": "hot",
  "timeRange": "all",
  "maxPostsPerSource": 100,
  "includeComments": false,
  "maxCommentsPerPost": 50,
  "minCommentsToFetch": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

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

Every harvested row in delivery order. 33 keys on each row; type is 'post' or 'comment'.

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

Counts per source, applied ceilings, refused communities and the charge totals for the run.

# 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": [
        "programming"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("santhej/reddit-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 = { "subreddits": ["programming"] }

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

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

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

```

## CLI example

```bash
echo '{
  "subreddits": [
    "programming"
  ]
}' |
apify call santhej/reddit-scraper --silent --output-dataset

```

## MCP server setup

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

```

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

## OpenAPI specification

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