Skip to main content

Command Palette

Search for a command to run...

I Built the Most Complete Skool API (Read AND Write) — and What I Learned Reverse-Engineering It

Skool has no official API. I spent 6 months reverse-engineering one — including writes, classroom management, and the auto-DM system. Here's what I learned.

Published
7 min read

TL;DR: I run a Skool community of 484 founders solo. To stay solo I needed to automate everything: member approval, course creation, comment replies, the welcome DM. Skool has no public API. So I reverse-engineered one and shipped it as an Apify actor — read AND write. This is what I learned.

Repo: github.com/ctala/skool-api-docs Actor: apify.com/cristiantala/skool-all-in-one-api

Why no official API?

Skool's been around since 2019. They've raised, grown, hosted Sam Ovens and a hundred other top creators. By any reasonable measure, they should have shipped an API by 2024.

They haven't.

Their public Help Center has a single line about it: "We're working on it." That line has been there since 2022.

Meanwhile, the Skool community-builder market is full of tools nobody can integrate. n8n? No native node. Make.com? Nothing. Zapier? Three half-broken Skool community integrations. Want to auto-approve members based on a LinkedIn check? You're hitting the dashboard manually, like it's 2015.

I needed automation. So I built it.

What "complete" means

Most existing Skool scrapers are read-only: they list posts, list members, sometimes paginate. None of them let you write.

This actor is the first I'm aware of that does:

ReadWrite
List posts (paginated, filtered, sorted)Create posts (with mentions, categories, video embeds)
Get full nested comment treesReply to posts. Reply to comments (nested)
List members + pending applicantsApprove, reject, ban, batch-approve
List courses, full classroom treeCreate courses, folders, pages from markdown
Read group settingsUpdate description, Auto DM, cover, icon
Upload images for course covers

That last block — classroom programmatic creation — is the one nobody had. You can pass a markdown directory tree and the actor builds the entire course in Skool, with TipTap-rendered bodies, brand-aligned covers, and tier gating.

If you're tired of clicking "New page" 60 times to launch a course, this is for you.

Reverse-engineering: 6 months in 5 lessons

Here's what burned me, in order, so it doesn't burn you.

Lesson 1: Posts and comments are the same object

This is the thing I wish someone had told me on day one.

In Skool's data model, a comment is a post with rootId and parentId:

Post:    { id, rootId: id,        parentId: null      }
Comment: { id, rootId: postId,    parentId: postId    }  ← top-level
Reply:   { id, rootId: postId,    parentId: commentId }  ← nested

There's no /comments endpoint. Everything goes through /posts. The post_type field disambiguates: "generic" for posts, "comment" for comments.

If you're building a Skool integration and you assume comments live under /comments (like every other platform), you'll waste a week looking for an endpoint that doesn't exist.

Lesson 2: Three different content formats

Skool uses three content formats depending on context:

ContextFormat
PostsPlain text
CommentsPlain text + mentions ([@Name](obj://user/{userId}))
Course pagesTipTap JSON, prefixed [v2]

If you send HTML to a post, Skool renders the tags literally. <p>Hello</p> shows up as <p>Hello</p>. I learned this by publishing a beautifully formatted weekly digest that looked like a markup tutorial to my members.

For course pages it's worse: Skool expects a literal string starting with [v2] followed by a JSON array of TipTap blocks. Like this:

[v2][{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Lesson 1"}]},...]

Building that JSON by hand is awful. So the actor includes a zero-deps markdown→TipTap converter. You write markdown, it ships TipTap. (More on this in lesson 5.)

Lesson 3: SSR for reads, REST for writes

Skool runs Next.js. Reads come from SSR data endpoints:

GET /_next/data/{buildId}/{slug}.json
GET /_next/data/{buildId}/{slug}/-/members.json
GET /_next/data/{buildId}/{slug}/-/pending.json

These are public-ish (they require auth cookies but no special API key). They're fast, paginated, and return clean JSON.

Writes go through https://api2.skool.com:

POST /posts?follow=true            ← create post
POST /posts?follow=false           ← create comment
POST /posts/{id}/update            ← edit (NOT PATCH/PUT — those return 405)
DELETE /posts/{id}                 ← delete
POST /members/{id}/role            ← approve/reject/ban
PUT /courses/{id}                  ← course/page operations

The buildId in those SSR URLs rotates roughly weekly when Skool deploys. The actor catches stale-buildId 404s, fetches the dashboard HTML to extract the new buildId, and retries. You don't see this — but if you build your own client, you'll see it within a week.

Lesson 4: Cookies, WAF, and the x402-payment-required confusion

Skool requires three cookies for every authenticated request:

auth_token       ← long-lived JWT (~1 year)
client_id        ← stable per session
aws-waf-token    ← rotates every ~3.5 days

The aws-waf-token is the painful one. It expires silently. Your perfectly-working integration just starts returning 403s on day 4.

Login is via Playwright headful: navigate, fill form, submit, wait for redirect, scrape cookies. Takes ~10 seconds. For production you cache the cookies and run login again every ~3 days.

Now the real fun: when something goes wrong on the Apify side, you get this error:

{"error": {"type": "x402-payment-required", ...}}

This is not a payment issue. It's Apify's generic response when the actor has its notice: UNDER_MAINTENANCE flag set. Apify auto-flips that flag when its Automated Daily Quality Check fails 2+ times in 3 days. Your actor's runs all return 402. Your users panic.

Took me 3 different incidents to figure out what was actually happening. The fix once you know:

curl -X PUT "https://api.apify.com/v2/acts/{actor_id}?token=$APIFY_TOKEN" \
  -d '{"notice": null}'

The prevention (which I learned the hard way): make sure your actor's INPUT_SCHEMA.json defaults make the daily quality check pass. The default action in this actor is now system:health — a no-auth, no-Skool, deterministic 1-item ping that always succeeds in <2 seconds. Apify's daily probe never fails. The flag never trips.

If you ship an Apify actor and don't think about this, you'll get bitten.

Lesson 5: PUT to courses silently resets privacy

This one almost made me delete the project.

I was batch-updating cover images for 12 production courses. Simple flat PUT:

PUT /courses/{courseId}
{"cover_image": "...", "cover_image_file": "..."}

Worked. Returned 200. I went to bed.

Next morning all 12 courses were public. Open. Free. The privacy field had been silently reset to 0.

Turns out: a partial PUT to a top-level course in Skool resets privacy to 0 (Open). min_tier stays. amount resets too. There's no error, no warning, no documentation. Skool just decides that anything you didn't send was "intentionally cleared."

The fix is a read-then-write pattern:

async updateCourse(input) {
  // Read current state
  const current = await this.getTree(input.courseId);
  const m = current.course.metadata;

  // Merge input on top of current
  const body = {
    title: input.title ?? m.title,
    desc: input.desc ?? m.desc,
    cover_image: input.coverImage ?? m.cover_image,
    cover_image_file: input.coverImageFile ?? m.cover_image_file,
    privacy: input.privacy ?? m.privacy ?? 0,
    min_tier: input.minTier ?? m.min_tier ?? 0,
  };

  // PUT the full body
  await this.http.put(`/courses/${input.courseId}`, body);
}

Costs +1 GET (~200ms) per update. Worth every millisecond.

If you're hand-rolling Skool API calls and you do PUTs, always re-send all fields, or accept that you'll eventually publicly leak premium content.

What's it cost?

The actor uses Apify's Pay-Per-Event model:

ActionApprox
Login (every 3.5 days)$0.02
Read action (list, get)$0.005
Write action (create, approve, etc.)$0.01 + $0.005

For a community handling ~50 writes + ~200 reads per day, that's around $1.50/month. The Apify free tier covers most personal/testing use.

(You're paying for Apify's compute, not for me. The actor is closed-source — that's how I monetize the work — but the docs, recipes, and example workflows in the docs repo are MIT.)

Quick start

# 1. Login once, save the cookies for ~3.5 days
curl -X POST "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "action": "auth:login",
    "email": "your@email.com",
    "password": "your-password",
    "groupSlug": "your-community"
  }'

# Response includes:
# {"cookies": "auth_token=...; client_id=...; aws-waf-token=...", "expiresInDays": 3.5}

# 2. Use cookies for every other action
curl -X POST "https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "action": "posts:list",
    "cookies": "auth_token=...; client_id=...; aws-waf-token=...",
    "groupSlug": "your-community",
    "params": {"page": 1}
  }'

Recipes for the most common workflows are in the docs:

Closing thought

Reverse-engineering an undocumented API is mostly archaeology: you read JS bundles, intercept network calls, build a model, watch it break, fix it, repeat. The fun part isn't the engineering, it's that I now run a 484-person community alone. The actor handles approvals while I sleep. Members get a personal-feeling welcome DM. Courses get published from markdown files in my git repo, with brand-aligned covers, automatically.

If you're a Skool community owner and you want this kind of leverage — the actor is the leverage. If you're a developer looking at Skool integrations, the docs repo has all the gotchas I learned the hard way, so you skip the bleeding.

Either way: don't click "Approve" 50 times this week. Build something.


Got questions or hit a bug? Open an issue on the docs repo. The actor is used in production daily so bugs get fixed fast.