# Skool Scraper - Communities, Pricing, Owners, Member Reviews (`snow_leo_data/skool-scraper`) Actor

2833 unique Skool communities from a sharded crawl where the catalogue stops at 1000. A community card carries 54 filled fields against 22 in a catalogue row, and member reviews come in full, 214 of 214. Skool leads scraper: community owners, paid plans, members count. No login. Public pages only.

- **URL**: https://apify.com/snow\_leo\_data/skool-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** For creators, Lead generation, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 communities

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

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Skool Communities Scraper

Skool.com hosts hundreds of thousands of paid communities, and its catalogue
will show you exactly one thousand of them per query. This Actor works around
that ceiling, returns the full community card for every row it finds, and can
attach the member reviews that no other Skool Actor in the store returns.

No Skool account, no cookies, no password, no proxy. Everything here comes
from pages and endpoints Skool serves to anyone.

#### What you get for one community

Every row is one community, and it carries what a buyer of this data actually
needs: how big it is, what it charges, who runs it, how to reach that person,
how alive it is, and what it is wired into.

- **Size and activity** - members, admins, posts, members online right now,
  rules, classroom courses, classroom modules.
- **Money** - free or paid, the amount, the currency, the billing interval,
  the price normalised to a monthly figure in dollars, the listed monthly and
  annual rates, the affiliate share the community pays, and an estimated
  monthly revenue where both a monthly price and a member count exist.
- **Owner** - name, handle, bio, location, time zone, personal links
  (website, YouTube, Instagram, X, LinkedIn, Facebook, TikTok), and Skool's
  own revenue tier badge for that owner.
- **Reputation** - average rating, review count, and on request every single
  review with its text, its author and whether that author is still paying.
- **Marketing stack** - which of Zapier, Google Ads, Meta Conversions API,
  Hyros, Skool Auto DM, webhooks and chat-level gating the community has
  switched on, plus the Google Ads tag itself.
- **Presentation** - landing page copy, landing images and videos, logo,
  cover, favicon, brand colour, the sections the community shows, the
  questions it asks people on the way in.

#### The ceiling, and how this Actor gets past it

Skool's catalogue is paginated 30 communities to a page and stops at 34 pages.
Page 35 comes back empty, and the `numGroups` field in the response never
promises more than 1000 no matter how many communities actually match. That is
the hard ceiling of one query, and every Actor that issues one query lives
under it.

The way past it is not a trick, it is arithmetic: **each search term and each
category is a separate query with its own separate ceiling.** Ask for the
catalogue plain and you see 1000 communities. Ask for it plain plus two
categories and the same crawl returns considerably more, because the overlap
between them is small.

Measured with `python3 tools/measure.py --sweep`, 3 queries, every page of
each: one plain query returns 1000 unique communities; the sharded crawl
returns 2833 unique communities. Same source, same day, same code - the only
difference is that the question was asked more than once. The 9 categories
Skool itself uses are `hobbies`, `music`, `money`, `spirituality`, `tech`,
`health`, `sports`, `self_improvement` and `relationships`, and search terms
can be anything you like, so the sharding goes as wide as you need it to.

#### The second layer: the community card

A row from the catalogue carries 22 filled fields. The community's own card
carries 54. The owner, the posts count, the course count, the rating, the
review count, the affiliate share, the marketing stack, the landing page copy
and the join survey exist only in the card, and the card is a separate request
per community.

`Fetch the full community card` is on by default because those fields are the
reason most people scrape this source at all. Turn it off and a run becomes
one request per 30 communities instead of one per community - useful when all
you want is a fast inventory of names, prices and member counts.

Communities you name directly under `Community URLs or handles` always get
their card fetched, whatever that switch says, because the catalogue knows
nothing about a community you name by hand.

#### Reviews, and why they matter more than the rating

Skool shows an average rating and a review count. This Actor can fetch the
reviews themselves, paginated 50 at a time - measured on one community with
214 declared reviews, all 214 came back, all of them distinct.

Each review carries the rating, the text, when it was written, the author's
handle and name, how long that author has been a member, and - this is the
part no summary gives you - **whether that author is still paying**. A
community with a 5.0 average built out of reviews by people who have all since
cancelled is a different proposition from one where the reviewers are still
subscribers, and no other field in this source tells you which one you are
looking at.

Reviews are attached to the community row as a list. They do not create rows
of their own, so on a pay-per-result price they do not multiply your bill.

#### Prices: read the field, not the flag

Skool has a field called `membershipModel` that looks exactly like a free/paid
flag. It is not one. Measured across 300 live catalogue rows, the same value
of that field appears both with a price and without one - 2 rows carried a
price while the flag was blank, and 39 carried no price with the flag equally
blank. Building a "paid only" filter on it would quietly hand you the wrong
list.

So this Actor reads the price itself, and from three places rather than one.
Most communities publish `display_price`. Some do not publish it at all and
still charge - they carry the rate in a separate field instead. Measured over
150 communities taken straight off the catalogue, 149 of them turned out to be
paid and 16 of those published no `display_price` at all: an Actor that reads
only that one field hands you 16 paid communities labelled free in every
hundred and fifty. Every row here says in `price_source` which field its price
came from, so nothing is guessed on your behalf.

Two more things the parser does that a naive one does not: amounts arrive in
cents inside a JSON string, so 900 means nine dollars; and a yearly plan is
divided by twelve before it is compared with a monthly one. A one-off purchase
is left without a monthly figure on purpose - inventing one would inflate the
revenue estimate of every community that sells a single course.

#### Filters, and the one rule behind all of them

Thirteen filters run on the source's own numbers: free or paid, minimum and
maximum monthly price, minimum and maximum members, minimum rating, minimum
reviews, minimum posts, minimum courses, minimum affiliate share, which
marketing tools are wired in, text in the name or description, and text inside
the reviews.

The rule behind every one of them: **an empty field passes.** If Skool did not
say how many members a community has, that community is not dropped by a
"minimum 100 members" filter. Absence of data is not a mismatch, and treating
it as one is how a single unfilled key at the source silently halves an order.

Filters that need the community card - rating, reviews, posts, courses,
affiliate share, marketing tools - are checked against the enrichment switch
**before any request is made**. Asking for "rating above 4.5" with the card
switched off cannot return anything, and you are told that in the log and in
the run report instead of paying for a crawl whose result was decided by the
input.

#### Monitor mode

Turn on `Monitor mode` and the Actor remembers what it delivered, in a named
key-value store that survives between runs. On the next run it writes only
what is new or what moved, and every changed row carries `changed_fields` - a
list of which values changed and what each one was before.

What counts as a change is deliberately narrow: members, price, price type,
posts, courses, review count, rating, name, description, owner and affiliate
share. A community's `updated_at` timestamp moves every time anyone posts in
it, so a fingerprint over the whole row would mark every active community as
changed on every run, and the mode would quietly become an ordinary crawl.

`Also write unchanged communities` keeps the memory but writes everything,
each row labelled NEW, UPDATED or UNCHANGED.

#### When the result is empty, it says why

A green run with an empty dataset is the worst thing a scraper can do to you.
This Actor never does it silently: the run report carries an `empty_reason`
and the log carries a sentence explaining it. There are 14 named reasons.

9 of them are decided by your input alone, before a single byte is
downloaded: `no-target-given`, `all-community-urls-invalid`,
`enriched-filter-without-enrichment`, `review-text-filter-without-reviews`,
`price-range-is-empty`, `members-range-is-empty`,
`free-only-with-a-minimum-price`, `category-not-recognised` and
`charge-limit-reached`.

Five can only be known after the crawl: `filtered-out` (with a per-filter
count of what dropped how many), `nothing-changed` (monitor mode working as
asked), `source-returned-nothing`, `all-communities-unreachable` and
`catalog-rate-limited`, which covers the case below.

#### How this Actor treats Skool's rate limit

The catalogue sits behind a CDN with a request budget per address. Measured
twice: after roughly 140 catalogue requests the whole catalogue path starts
answering 403 while the community endpoint keeps working normally, and the
block clears on its own after 423 seconds. Pacing the requests does not avoid
it - the second measurement spaced them two seconds apart and got blocked at
the same count.

So the Actor stops before the CDN stops it. One run makes at most 120
catalogue requests, which is up to 3600 communities, and if it reaches that
ceiling it says so in the report rather than passing a short result off as a
complete one. Community cards and reviews are not affected: they come from a
different host that answered 16 rapid requests in a row without a single
complaint.

There is one more thing the CDN does that matters. The human-facing catalogue
page answers datacentre addresses with HTTP 202 and a two-kilobyte holding
page - a success code with no data in it. This Actor asks the catalogue's own
data endpoint instead, verifies it received communities rather than a status
code, and treats a holding page as the failure it is.

#### Defaults, and what a default run costs

Out of the box the Actor asks the `money` category for 50 communities with
their cards. That run finished in under ten seconds in the cloud.

The defaults are set so that nothing surprises you: 6 community cards are fetched at once, the timeout per request is 30 seconds, the crawl goes 120 communities per query deep, reviews are off, monitor mode is off, and compact output is off.

`Compact rows` drops every empty field from every row. Useful for AI agents
and for spreadsheets that struggle with wide sparse tables.

`Community handles from another dataset` takes handles or links out of another
Actor's dataset, so this one can be chained after any crawl that produced
Skool links.

#### Honest limitations

Things this Actor does not do, and why. None of these are oversights.

- **No posts, comments or classroom lessons.** Skool serves those only to a
  signed-in member of that community. Other Actors do it by asking you for
  your session cookies or your Skool password; this one asks for no
  credentials at all. Course and module counts are public, and those you get.
- **No member lists.** Same reason - the member directory is behind
  membership.
- **No file or video downloads.** Nothing downloadable is public here.
- **Owner email is usually blank.** The field exists in Skool's data and is
  returned when filled, but across every community checked it came back empty.
  If an email address matters to you, the owner's personal website and social
  links are in the row and usually lead to one.
- **One run sees at most 3600 communities**, for the CDN reason above. A
  larger inventory means several runs, which is also how the source expects to
  be read.
- **Estimated monthly revenue is an estimate**, computed as monthly price
  times member count. It cannot know about annual plans bought at a discount,
  free members inside a paid community, or churn. It is labelled as an
  estimate everywhere it appears.
- **Search matches what Skool matches.** Terms are matched by Skool against
  community names and descriptions, so a narrow phrase can legitimately return
  nothing, and that case is reported as `source-returned-nothing` rather than
  dressed up as an error.

#### How it compares with the other Skool Actors

The Skool niche in the Apify store holds 70 Actors and 4376 users between
them. `COMPARISON.md` in this repository is a line-by-line comparison against
the five largest, including every input they have that this Actor does not.

The short version. The widest of them,
`gordian/skool-group-scraper`, publishes 67 output fields; this Actor declares
78 and covers every one of theirs. None of the others return member reviews,
none name a reason for an empty dataset, none catch a self-contradicting input
before downloading, and none shard the catalogue to get past the 1000 ceiling.
What they have and this one does not is the logged-in half of Skool: posts,
comments, members and classroom content.

#### FAQ

**Do I need a Skool account?**
No. Nothing here touches a login. That is also why posts and member lists are
out of scope.

**Can I get every community on Skool?**
Not in one run, and not from this source at all. The catalogue caps every
query at 1000 and the CDN caps an address at about 140 catalogue requests.
Several runs with different search terms and categories will build a large
inventory; one run will not.

**How do I find communities in a niche?**
Put words in `Search terms`. Each term is its own query, so five terms give
you five separate catalogue budgets rather than one shared one.

**How do I find the big earners?**
Ask for paid communities only, set a minimum member count, and sort the result
by `estimated_mrr_usd`. For a second opinion that is not an estimate, look at
`owner_revenue_tier`: that badge is Skool's own assessment of how much the
owner earns on the platform.

**Why is a community I know is paid marked free?**
It should not be, and that was a real bug found on a cloud run. Prices are now
read from three fields and `price_source` tells you which one answered. If you
find a community where this is still wrong, the row will show
`price_source: null`, which makes it easy to spot.

**Why is my run returning fewer communities than I asked for?**
Check the run report. `catalog_budget_reached` means the Actor stopped itself
before the CDN did. `filtered_out` means your filters removed them and names
which filter removed how many. `empty_reason` explains the rest.

**Can I run this on a schedule?**
Yes, and monitor mode is built for exactly that: the second run writes only
what changed, so a daily schedule does not bill you for the same unchanged
communities every day.

**Does asking for reviews cost more rows?**
No. Reviews are attached to the community row as a list, so the number of rows
in your dataset is the number of communities, with or without reviews.

**What is `found_via`?**
Which query produced that row - a search term or a category. Useful when you
shard widely and want to know which angle actually found the interesting ones.

# Actor input Schema

## `searchTerms` (type: `array`):

Words to search the Skool catalogue for. Every term is a separate query with its own 1000-community ceiling, so several terms see more than one ever can. Matched by Skool against community names and descriptions.

## `categories` (type: `array`):

Skool's own nine categories. Each one is a separate query with its own ceiling: hobbies, music, money, spirituality, tech, health, sports, self\_improvement, relationships.

## `communityUrls` (type: `array`):

Named communities to fetch directly, as a full link (https://www.skool.com/vegan) or a bare handle (vegan). These are always fetched from the community card, whatever the enrichment switch says.

## `maxCommunitiesPerQuery` (type: `integer`):

How deep to go into each search term or category before moving to the next one. The catalogue never returns more than 1000 per query (34 pages of 30), so anything above that is capped.

## `enrichCommunities` (type: `boolean`):

On: every community found in the catalogue is also fetched from Skool's community endpoint - 47 filled fields instead of 21, including the owner, posts, courses, rating, affiliate percent and marketing tools. Off: catalogue fields only, one request per 30 communities.

## `reviewsPerCommunity` (type: `integer`):

How many member reviews to attach to each community. Reviews carry the rating, the text, the author and whether that author is still paying. 0 means none. Reviews do not add rows, so they do not add cost per result.

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

Stop after this many communities have been DELIVERED, not merely looked at - so a narrow filter still fills the quota. 0 means no limit.

## `priceType` (type: `string`):

Keep only free communities, only paid ones, or both. Decided by the price the community actually publishes, never by Skool's membership model number - that number carries a price in some communities and not in others.

## `minMonthlyPriceUsd` (type: `number`):

Keep communities costing at least this much per month. Yearly plans are converted to a monthly figure first. 0 switches the filter off.

## `maxMonthlyPriceUsd` (type: `number`):

Keep communities costing at most this much per month. 0 switches the filter off.

## `minMembers` (type: `integer`):

Keep communities with at least this many members. 0 switches the filter off.

## `maxMembers` (type: `integer`):

Keep communities with at most this many members. 0 switches the filter off.

## `minRating` (type: `number`):

Keep communities rated at least this highly by their own members. Needs the community card, so it only works with enrichment on.

## `minReviews` (type: `integer`):

Keep communities with at least this many member reviews. Needs the community card.

## `minPosts` (type: `integer`):

Keep communities with at least this many posts - the plainest measure of whether a community is alive. Needs the community card.

## `minCourses` (type: `integer`):

Keep communities with at least this many classroom courses. Needs the community card.

## `minAffiliatePercent` (type: `integer`):

Keep communities paying at least this share to affiliates. Needs the community card.

## `marketingTools` (type: `array`):

Keep communities wired to at least one of these: Zapier, Google Ads, Meta Conversions API, Hyros, Skool Auto DM. Needs the community card.

## `nameContains` (type: `string`):

Keep communities whose handle, name, description or landing page contains this text. Case is ignored.

## `reviewTextContains` (type: `string`):

Keep communities where at least one fetched review contains this text. Needs Reviews per community above zero.

## `onlyChanged` (type: `boolean`):

Remember what was delivered and, on later runs, write only communities that are new or whose numbers moved - members, price, posts, courses, rating, owner. Every row says which fields changed and what they were before.

## `emitUnchanged` (type: `boolean`):

Keep the memory of monitor mode but write every community anyway, each labelled NEW, UPDATED or UNCHANGED.

## `compact` (type: `boolean`):

Drop empty fields from every row. Smaller payload for AI agents and spreadsheets that choke on wide sparse tables.

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

How many community cards are fetched at once. The catalogue itself is always paced at a steady rate, because Skool's CDN blocks an address that asks too fast.

## `timeoutSeconds` (type: `integer`):

How long to wait for one response before retrying it.

## `inputDatasetId` (type: `string`):

Take community handles or links from the dataset of another run, so this Actor can be chained after any other one.

## `urlField` (type: `string`):

Which field of that dataset carries the community link or handle.

## Actor input object example

```json
{
  "searchTerms": [
    "ai automation"
  ],
  "categories": [
    "money"
  ],
  "communityUrls": [],
  "maxCommunitiesPerQuery": 120,
  "enrichCommunities": true,
  "reviewsPerCommunity": 0,
  "maxItems": 50,
  "priceType": "any",
  "minMonthlyPriceUsd": 0,
  "maxMonthlyPriceUsd": 0,
  "minMembers": 0,
  "maxMembers": 0,
  "minRating": 0,
  "minReviews": 0,
  "minPosts": 0,
  "minCourses": 0,
  "minAffiliatePercent": 0,
  "marketingTools": [],
  "nameContains": "",
  "reviewTextContains": "",
  "onlyChanged": false,
  "emitUnchanged": false,
  "compact": false,
  "concurrency": 6,
  "timeoutSeconds": 30,
  "inputDatasetId": "",
  "urlField": "url"
}
```

# Actor output Schema

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

All collected rows

# 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 = {
    "searchTerms": [
        "ai automation"
    ],
    "categories": [
        "money"
    ],
    "communityUrls": [],
    "marketingTools": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/skool-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 = {
    "searchTerms": ["ai automation"],
    "categories": ["money"],
    "communityUrls": [],
    "marketingTools": [],
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/skool-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 '{
  "searchTerms": [
    "ai automation"
  ],
  "categories": [
    "money"
  ],
  "communityUrls": [],
  "marketingTools": []
}' |
apify call snow_leo_data/skool-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/skool-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/5lSyOGcjEcpyuKM1c/builds/8otNwwaX9dsF825zv/openapi.json
