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.
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:
| Read | Write |
| List posts (paginated, filtered, sorted) | Create posts (with mentions, categories, video embeds) |
| Get full nested comment trees | Reply to posts. Reply to comments (nested) |
| List members + pending applicants | Approve, reject, ban, batch-approve |
| List courses, full classroom tree | Create courses, folders, pages from markdown |
| Read group settings | Update 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:
| Context | Format |
| Posts | Plain text |
| Comments | Plain text + mentions ([@Name](obj://user/{userId})) |
| Course pages | TipTap 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:
| Action | Approx |
| 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:
- Auto-approve Skool members with GPT-4o AI screening — ready-to-import n8n template
- Publish a course from markdown
- Auto DM new members
- Reply to unanswered posts
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.
