Skool API — Posts, Members & Classroom avatar

Skool API — Posts, Members & Classroom

Pricing

from $5.00 / 1,000 results

Go to Apify Store
Skool API — Posts, Members & Classroom

Skool API — Posts, Members & Classroom

Skool API to automate posts, comments, members and classroom courses on any Skool community where you're admin. Read AND write access. Drop-in for n8n, Make.com, Zapier, AI agents (Claude, ChatGPT,LangChain). Bulk approve members, scrape comment threads, auto-DM, publish courses from markdown.

Pricing

from $5.00 / 1,000 results

Rating

5.0

(1)

Developer

Cristian Tala S.

Cristian Tala S.

Maintained by Community

Actor stats

3

Bookmarked

43

Total users

17

Monthly active users

4.9 days

Issues response

4 days ago

Last modified

Share

Skool API — Automate Posts, Members, Comments & Classroom on Skool.com

The most complete Skool API on Apify. Read AND write access to Skool community posts, comments, members, courses (classroom), file uploads, Auto DM and group settings — for any Skool community where you have admin rights. Production-ready Skool automation for n8n, Make.com, Zapier, Pipedream, custom backends, AI agents (Claude, ChatGPT, OpenClaw, LangChain), and Skool community scrapers.

If you're trying to automate your Skool community, bulk approve Skool members, scrape Skool posts and comments, send Auto DMs to new members, publish courses programmatically, or integrate Skool with n8n — this is the only actor on Apify that does it all. Stop fighting Cloudflare, WAF tokens, and Skool's missing public API. This actor handles the entire automation layer.

Use cases: community onboarding automation, member screening with AI, content cross-posting, classroom course publishing from markdown, scheduled posts, comment threading, member analytics, course resource management. Used in production by CAR (Cágala, Aprende, Repite), 480+ members.

What You Can Build With This Skool API

Use caseActions usedWhy this works
Auto-approve Skool members with AI screeningmembers:pending + AI scoring + members:approve / members:rejectStop manually approving every signup. Filter spam, gauge intent, save hours per week. (n8n template)
Auto-DM every new Skool membergroups:setAutoDMOne-time setup, runs forever inside Skool. Onboarding nudge with personalized welcome (#NAME#, #GROUPNAME# tokens).
Cross-post blog → Skool community feedposts:create + groups:get (for labelId)Syndicate your content automatically. Each newsletter/blog post = 1 community post, fully formatted.
Bulk reply to unanswered Skool postsposts:list + filter commentCount === 0 + posts:createCommentStop letting questions sit unanswered. Auto-reply with AI when comments stale > 24h.
Scrape ALL comments in a Skool thread (>35)posts:getCommentsFullSkool REST caps at ~35 comments per thread. This Playwright-based action bypasses that — get the full thread for analysis or response automation.
Publish a complete Skool course from markdownclassroom:createCourse + classroom:createFolder + classroom:createPage + classroom:setBodyWrite courses as .md files in Git. Push → Skool course updated. Markdown → TipTap converter built-in.
Member analytics dashboard for Skoolmembers:list + posts:list + custom export to NocoDB/AirtableApify dataset → your BI tool. Track activity, engagement, churn signals.
Skool community moderation botposts:filter + LLM classification + posts:delete / members:banDetect spam, off-topic, or policy violations programmatically. Human approval optional.
Welcome challenge for new membersgroups:setAutoDM + posts:createComment + members:listDay 1: introduce yourself. Day 3: first win. Day 7: share a result. Drives early activation.
Cross-community member migrationmembers:list (source) + members:approve (destination)Move members between Skool communities you own without manual one-by-one.

📚 Full docs, tutorials & recipes: github.com/ctala/skool-api-docs⭐ Star + Watch the repo to get notified of new releases.

🚀 Ready-to-import n8n template: Auto-approve Skool members with GPT-4o AI screening

📝 Technical deep-dives (Hashnode):

What's new in 0.3.9 (May 2026): Two new actions for lesson resources (the file-download UI inside any classroom page): files:uploadFile registers a private file (PDF, JSON, ZIP, etc.) with the right privacy:1 flag Skool requires for resources, and classroom:updateResources attaches one or more uploaded files to a lesson under the Resources section. Discovered via UI capture: Skool stores resources at the top level of the body (not in metadata.attachments as the obvious shape suggests) and silently rejects files registered with the default privacy:0. Also fixes the stale buildId auth path: Skool removed /dashboard from its Next.js routes, so we now refresh the build id from / (homepage) with /about as fallback. Includes a public validateBuildId() helper for proactive checks. Earlier in 0.3.x: classroom:updateCourse is read-then-write (preserves privacy/minTier/amount), markdown callout fixes, structured error hints (MISSING_CATEGORY, TITLE_TOO_LONG, INSUFFICIENT_TIER), never-throw guarantees, and quality-check-safe defaults to prevent Apify's UNDER_MAINTENANCE flag.

What makes this different

  • Read AND Write — Create posts, edit comments, approve pending members, build entire courses (modules, folders, lessons), upload covers, edit Auto DM. Not just scraping.
  • Fast cookie-based auth — Login once via Playwright (~10s), reuse cookies for ~3.5 days (~2s per call after that). Or pass email + password every call (slower, simpler).
  • Nested comments — Full comment trees with replies, not flat lists.
  • User mentions — Tag users with [@Name](obj://user/{id}).
  • Markdown → Skool TipTap — Built-in zero-deps converter for course bodies. Handles headings, bold/italic/code/links, blockquote callouts, bullet/ordered lists, code blocks, and simple tables.
  • AI-agent friendly — Clean action:operation format, structured failure payloads with recovery hint, perfect for n8n, Make.com, OpenClaw, Claude, ChatGPT plugins, LangChain agents, and any LLM tool-calling layer.
  • Production-tested — Powers cagala-aprende-repite (Cágala, Aprende, Repite) with 480+ members and live newsletter / classroom / member pipelines.

How much does it cost?

ScenarioEstimated cost
Login (once every ~3.5 days)~$0.02 (Playwright)
List 1 page of posts (~32 posts)~$0.005
Get comments on a post (REST, max ~35)~$0.005
Get ALL comments via Playwright scroll~$0.05 scrape fee + results
Create a post~$0.01 (write fee) + ~$0.005
Reply to a comment~$0.01 (write fee) + ~$0.005
Approve 10 pending members~$0.10 (write fees) + ~$0.005

Platform compute costs (~$0.002/run) included. Write operations have premium pricing.

Step 1: Login once — get cookies

{
"action": "auth:login",
"email": "your@email.com",
"password": "your-password",
"groupSlug": "your-community"
}

Output:

{
"success": true,
"cookies": "auth_token=eyJ...; client_id=abc...; aws-waf-token=xyz...",
"expiresAt": "2026-03-30T06:00:00.000Z",
"expiresInDays": 3.5,
"note": "Pass this cookies string in subsequent calls to skip Playwright login."
}

Step 2: Use cookies for all operations (fast, ~2s each)

{
"action": "posts:list",
"cookies": "auth_token=eyJ...; client_id=abc...; aws-waf-token=xyz...",
"groupSlug": "your-community",
"params": { "page": 1 }
}

Step 3: When cookies expire (~3.5 days) → login again

If you get an error about expired auth, simply run auth:login again.

Alternative: email + password every time (slower)

You can skip Step 1 and pass email + password directly in any action. This uses Playwright every time (~10s instead of ~2s), but it's simpler for one-off operations.

{
"action": "posts:list",
"email": "your@email.com",
"password": "your-password",
"groupSlug": "your-community",
"params": { "page": 1 }
}

Input Reference

FieldRequiredDescription
actionAlwaysWhat to do: auth:login, posts:list, posts:createComment, etc.
groupSlugAlwaysCommunity slug from URL: skool.com/{slug}
cookiesOption ACookie string from auth:login. Fast, no browser.
emailOption BSkool email. Uses Playwright, slower. Required for auth:login.
passwordOption BSkool password. Required for auth:login.
paramsVariesAction-specific parameters (see below).

Actions Reference

Authentication

ActionAuthDescription
auth:loginemail + passwordLogin via Playwright, returns cookies for reuse. Run once every ~3.5 days.

Posts (read)

ActionParamsDescription
posts:listpage? (default 1), sortType?List posts from community feed
posts:getpostIdGet single post by ID
posts:getCommentspostIdGet comment tree (REST, fast ~2s, capped at ~35 comments due to Skool API limit)
posts:getCommentsFullpostId, postSlugGet ALL comments in a thread via Playwright + DOM scroll. Bypasses Skool's ~35 comment REST cap. $0.05 scrape fee per invocation (slower, ~30-60s)

Posts (write — $0.01 per operation)

ActionParamsDescription
posts:createtitle, content, labelId?Create new post
posts:updatepostId, title?, content?Edit post or comment
posts:deletepostIdDelete post or comment
posts:pinpostIdPin post to top
posts:unpinpostIdUnpin post
posts:votepostId, vote ("up" or "")Like or unlike
posts:createCommentcontent, rootId, parentIdReply to post or comment

Members (read)

ActionParamsDescription
members:listpage?List active members
members:pendingList pending approval requests

Members (write — $0.01 per operation)

ActionParamsDescription
members:approvememberIdApprove pending member
members:rejectmemberIdReject pending member
members:banmemberIdBan member

Classroom — courses, folders, pages, TipTap bodies

UI Skool renders 3 levels: Course (top tile) → Page direct (unit_type:"module") OR Folder (unit_type:"set") → Page. Modules nested in modules are invisible.

ActionParamsDescription
classroom:listCoursesList all courses in the group
classroom:getTreecourseIdRecursive tree for a top-level course
classroom:createCoursetitle, desc?, coverImage?, coverImageFile?, privacy? (0/1/2/3/4), minTier?, amount? (USD cents, for privacy=3), drip? {enabled,days} (for privacy=4), affiliateCommissionEligible?, state?Create top-level course
classroom:createFolderparentCourseId, title, state?Create folder (unit_type:"set") inside a course
classroom:createPagecourseId, parentId (course or folder id), title, state?Create page (unit_type:"module")
classroom:setBodypageId, title, bodyMarkdown? OR bodyRaw?, videoId?, transcript?Set page body — bodyMarkdown is converted to TipTap [v2] automatically
classroom:updateCoursecourseId, title?, desc?, coverImage?, coverImageFile?, privacy? (0/1/2/3/4), minTier?, amount?, transcript?, videoId?Read-then-write update. Any field you don't pass is preserved automatically (the actor reads the course first, merges, then writes). Pass privacy/minTier/amount to change them; omit them to keep the current values. Avoids the Skool quirk where partial PUTs reset privacy to 0.
classroom:deleteUnitidDelete course / folder / page (cascades children)
classroom:updateResourcescourseId, pageId, resources (array of {title, file_id}, max ~34 chars per title)Replace the Resources list shown under a lesson page (the Add resource file UI). Pass [] to clear all. file_id MUST come from files:uploadFile (not uploadImage) — Skool rejects privacy:0 files with 400 invalid file ... privacy: 0. Skool replaces the full list (no patch semantics) on every call.

privacy values: 0 Open · 1 Level unlock (uses minTier as gamification level) · 2 Private · 3 Buy now (requires amount, may use minTier as paid tier override) · 4 Time unlock (requires drip).

bodyMarkdown accepts standard markdown: headings, bold, italic, code, url, > blockquotes (lists nested inside callouts are flattened to so the callout border stays cohesive — Skool only borders paragraph children), bullet/ordered lists, ``` code blocks, simple tables. Output is the literal [v2]<JSON> Skool's desc field expects. Zero deps; converter ported in-tree.

Files — image + private file upload

ActionParamsDescription
files:uploadImagebufferBase64 OR imageUrlUpload a public image; returns {coverImageUrl, coverImageFile} ready for classroom:createCourse. Re-encoded server-side (PNG/WebP → JPG). Recommended dimension: 1460×752
files:uploadFilefilename + (bufferBase64 OR fileUrl OR text), optional contentType (default application/octet-stream)Upload a private file (PDF, JSON, ZIP, etc.) with privacy:1. Returns {fileId, contentType, fileName}. Pair with classroom:updateResources to attach to a lesson

The two file actions differ by privacy: uploadImage registers a public-read file (used as a cover URL); uploadFile registers a private file (signed download URL generated on demand by Skool). Don't try to pass an uploadImage file id into updateResources — Skool rejects it.

Groups — config + Auto DM

ActionParamsDescription
groups:getslugGroup object (id, metadata) — uses Next.js SSR endpoint
groups:setAutoDMmessageUpdate Auto DM new members template. Tokens: #NAME#, #GROUPNAME#. UI limit: 300 chars

Content Format

Posts and comments use plain text, NOT HTML.

CORRECT: "Hello world! Great post."
WRONG: "<p>Hello world! Great post.</p>"

Mentioning Users

Tag users in posts and comments:

[@Display Name](obj://user/{userId})

Example: Hey [@John Smith](obj://user/abc123def456...)! Welcome.

Get user IDs from members:list.

Important: Posts = Comments

In Skool, posts and comments are the same object.

  • Reply to a post: rootId = postId, parentId = postId
  • Reply to a comment (nested): rootId = postId, parentId = commentId
  • Edit a comment: use posts:update with the comment's ID
  • Delete a comment: use posts:delete with the comment's ID

There is no comments: namespace. Everything is posts:.

Example Workflows

Auto-approve pending members

1. auth:login → save cookies
2. members:pending → get list of pending members
3. For each: members:approve with memberId

Reply to unanswered posts

1. auth:login → save cookies
2. posts:list → filter by commentCount === 0
3. For each: posts:createComment with rootId = postId, parentId = postId

Create post with user mention

{
"action": "posts:create",
"cookies": "...",
"groupSlug": "my-community",
"params": {
"title": "Weekly Update",
"content": "Great work this week [@John Smith](obj://user/abc123...)! Keep it up.",
"labelId": "category-id"
}
}

Get comment thread for a post (REST, max ~35 comments)

{
"action": "posts:getComments",
"cookies": "...",
"groupSlug": "my-community",
"params": { "postId": "32-char-hex-post-id" }
}

Returns nested tree:

[
{
"id": "comment-1",
"content": "Great post!",
"replies": [
{ "id": "reply-1", "content": "Thanks!", "replies": [] }
]
}
]

Get ALL comments in a thread (Playwright scroll, no ~35 cap)

For threads with hundreds of comments (welcome threads, popular posts, AMAs), Skool's REST API caps at ~35. Use posts:getCommentsFull to scrape the full thread via headless browser scroll. $0.05 scrape fee per invocation (slower, ~30-60s) but returns every comment.

{
"action": "posts:getCommentsFull",
"cookies": "...",
"groupSlug": "my-community",
"params": {
"postId": "32-char-hex-post-id",
"postSlug": "url-slug-of-the-post"
}
}

Returns flat array of all comments (top-level + nested replies):

[
{ "id": "scraped-0", "content": "Hi everyone! I'm Jane...", "author": { "firstName": "Jane", "lastName": "Doe" }, "isReply": false },
{ "id": "scraped-1", "content": "Welcome Jane!", "author": { "firstName": "Admin" }, "isReply": true },
...
]

When to use which:

  • posts:getComments — fast, free, returns first ~35 comments. Use for new threads and most cases.
  • posts:getCommentsFull — slow + paid, returns ALL. Use only when you know the thread has >35 comments and need full coverage.

Attach a downloadable file to a lesson page (Resources)

Two-step flow: upload the file with privacy:1, then attach it via updateResources. The same file_id can be reused across pages.

// Step 1 — upload the JSON workflow (or PDF / ZIP / etc.)
{
"action": "files:uploadFile",
"cookies": "...",
"groupSlug": "my-community",
"params": {
"filename": "my-workflow.json",
"contentType": "application/json",
"text": "{\n \"name\": \"My workflow\",\n \"nodes\": [],\n \"connections\": {}\n}"
}
}
// → { "fileId": "63a16fa2d8f3472d8609e469e13a6bcc", "fileName": "my-workflow.json", "contentType": "application/json" }
// Step 2 — attach to a lesson under "Resources"
{
"action": "classroom:updateResources",
"cookies": "...",
"groupSlug": "my-community",
"params": {
"courseId": "TOP_LEVEL_COURSE_ID",
"pageId": "LESSON_PAGE_ID",
"resources": [
{ "title": "W-01 · Auto-approve members", "file_id": "63a16fa2d8f3472d8609e469e13a6bcc" }
]
}
}

Pass "resources": [] to clear all attachments. Skool replaces the entire list on every call (no patch semantics). title is capped to ~34 chars by the UI. Use bufferBase64 instead of text for binary files.

Output

Post object

{
"id": "32-char-hex",
"title": "Post Title",
"content": "Plain text content",
"author": { "id": "...", "firstName": "John", "lastName": "Smith", "slug": "john-smith" },
"createdAt": "2026-03-26T18:00:00Z",
"likes": 5,
"commentCount": 12,
"isPinned": false,
"url": "https://www.skool.com/community/post-slug"
}

auth:login output

{
"success": true,
"cookies": "auth_token=...; client_id=...; aws-waf-token=...",
"expiresAt": "2026-03-30T06:00:00Z",
"expiresInDays": 3.5
}

Error Handling

Starting in v0.3.0, runs NEVER exit with exit_fail. Every error — recognized or not — becomes a structured {success:false} payload pushed to the dataset, and the run terminates with SUCCEEDED. This is intentional: an exit_fail feeds Apify's heuristic that flips the actor's notice: UNDER_MAINTENANCE flag, which then breaks production workflows silently. The cost of structured failures in the dataset is much lower than a flag flip.

Your integration code MUST inspect dataset[0].success before assuming a result.

Failure payload shape (dataset)

{
"success": false,
"action": "posts:createComment",
"error": "post not found: 3bc910b1",
"errorCode": "NOT_FOUND",
"errorCategory": "not_found",
"statusCode": 404,
"retryable": false,
"hint": "Verify the ID provided in params. If it was valid before, the resource may have been deleted."
}

Error categories

errorCategoryMeaningTypical retryableWhat to do
input_validationMissing/bad params (no postId, no action, etc.)falseFix the actor input and rerun
auth_errorLogin failed, captcha, bad password, AUTH_ERROR, WAF_EXPIREDtrueRe-run auth:login to get fresh cookies
not_foundNotFoundError from Skool (post/member not found)falseVerify the ID; resource may have been deleted
rate_limited429 from SkooltrueBack off and retry after a few minutes
skool_api_error4xx from Skool other than above (e.g. "cannot update to same role", missing labelId)usually falseInspect error and fix params; if 5xx, retry later
scraping_errorBuildIdStaleError when Skool dashboard HTML changedtrueRe-run auth:login to refresh buildId
network_errorFetch timeout, abort, DNS failure, connection reset (AbortError/TimeoutError/ECONN*)trueTransient. Retry after 30-60s. If it persists for >5 min, Skool or your network is down
unknown_errorAnything not classified above (real bug). Includes stack field for diagnosistrueInspect the stack in the payload + Apify run logs. Open an issue if reproducible

Recognising a failure in your caller

const items = await fetchDatasetItems(runId);
const first = items[0];
if (first.success === false) {
console.error(`[${first.errorCategory}/${first.errorCode}] ${first.error}`);
if (first.retryable) {
// back off, refresh auth, or retry — based on errorCategory
} else {
// surface to user, adjust input
}
return;
}
// success path

Common errors → how to fix

Error message / errorCodeCauseSolution
INPUT_VALIDATION "Missing required params.postId"Missing fieldAdd the field in params
AUTH_ERROR "Login failed — still on login page after 30s"Wrong password, WAF challenge, or Skool rate-limited the loginVerify credentials; try again from a browser first
WAF_EXPIREDCookies older than ~3.5 daysRe-run auth:login
BUILDID_STALESkool deployed a new dashboardRe-run auth:login
NOT_FOUNDInvalid postId / memberIdDouble-check the ID source
RATE_LIMITHit 20-30 writes/min ceilingWait 60-120s, then retry
SKOOL_API_ERROR (400) "cannot update to same role"Member already has that roleNo-op: skip
SKOOL_API_ERROR (422) "must select category"Community requires labelIdPass labelId in params

Integration Examples

n8n Workflow

  1. HTTP Request nodeauth:login → save cookies to variable
  2. HTTP Request nodeposts:list with cookies → process posts
  3. IF node → filter unanswered posts
  4. HTTP Request nodeposts:createComment with cookies

Apify API (JavaScript)

// Login once
const loginRun = await fetch('https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/runs?token=YOUR_TOKEN', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'auth:login', email: '...', password: '...', groupSlug: '...' })
}).then(r => r.json());
// Get cookies from dataset
const items = await fetch(`https://api.apify.com/v2/actor-runs/${loginRun.data.id}/dataset/items?token=YOUR_TOKEN`).then(r => r.json());
const cookies = items[0].cookies;
// Use cookies for fast operations
const listRun = await fetch('https://api.apify.com/v2/acts/cristiantala~skool-all-in-one-api/runs?token=YOUR_TOKEN', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'posts:list', cookies, groupSlug: '...', params: { page: 1 } })
}).then(r => r.json());

📚 Tutorials, recipes & docs

Full reference + production-grade recipes: github.com/ctala/skool-api-docs

Recipes (copy-paste integrations):

Get notified of new releases: Watch the docs repo — every release ships with a CHANGELOG.md entry.

Roadmap

  • Posts CRUD (create, read, update, delete)
  • Comments with nesting (reply to posts and comments)
  • Members (list, pending, approve, reject, ban, batchApprove)
  • User mentions
  • Cookie-based fast auth
  • Courses (classroom) — read, create, update, delete, markdown→TipTap converter
  • File uploads (cover images, group icon/cover)
  • Auto DM new members (groups:setAutoDM)
  • Group description (read + update)
  • Events — list, create, RSVP
  • Analytics — engagement, revenue, member growth
  • Chat / DMs
  • Search
  • Apply form questions (read + update)
  • Discovery keywords (read + update)
  • "Send email to all members" toggle on posts:create

FAQ — Skool API Common Questions

Does Skool.com have an official API?

No. Skool does not provide a public API. This actor reverse-engineers Skool's internal endpoints (Next.js SSR for reads, api2.skool.com for writes) and exposes them as a clean, AI-friendly API. It's the production-ready alternative to scraping with Playwright or manually copy-pasting from the Skool UI.

Can I automate my Skool community without coding?

Yes. The actor is designed for n8n, Make.com, Zapier, and Pipedream — all no-code platforms. There's a ready-to-import n8n template for auto-approving members with AI screening. Just connect via HTTP Request nodes (3-4 nodes per workflow). No backend needed.

How does the Skool API handle authentication?

Two options: (1) Email + password every call (uses Playwright, ~10s per run, simpler), or (2) Cookie reuse (run auth:login once → save the cookies string → pass in subsequent calls for ~2s response time, valid ~3.5 days). Cookies expire because of Skool's WAF token rotation. The actor exposes auth:login so you can re-authenticate programmatically when needed.

Can I read AND write to Skool, or only read?

Both. Unlike scrapers, this actor performs full CRUD operations: create posts, edit comments, approve members, ban users, publish entire courses (classroom), upload covers, set Auto DM, update group descriptions. The write operations require admin or moderator rights in the target community.

How many Skool comments can I retrieve from a thread?

Skool's REST API caps at ~35 comments per thread (25 head + 10 tail). For threads with hundreds of comments (welcome threads, AMA posts), use posts:getCommentsFull which uses Playwright to scroll the full DOM and return every comment. This costs $0.05 per invocation due to compute cost but bypasses the cap entirely.

How does the Skool API integrate with AI agents (Claude, ChatGPT, LangChain)?

Every action follows a clean namespace:operation format ideal for function calling / tool use. The structured failure payloads include errorCode, errorCategory, and a recovery hint — agents can self-correct on errors. Use cases: AI member screening, auto-classification of posts, AI-generated welcome replies, automated content moderation. Compatible with OpenAI function calling, Anthropic tool use, LangChain Tool, Google Gemini, MCP.

Can I use this Skool API with multiple communities?

Yes. Each call takes a groupSlug parameter, so you can operate across multiple Skool communities where you're admin. Common pattern: one set of cookies per admin account, then switch groupSlug per call to target different communities.

What's the cost of running the Skool API?

See pricing table at the top of this README. Roughly: $0.005 per dataset result, $0.01 per write operation (create/update/delete/approve/etc.), $0.05 per Playwright scrape operation, and $0.01 actor start fee per run. A typical "auto-approve 10 pending members" run costs ~$0.10. Cookie auth means you only pay the Playwright fee once every 3.5 days.

Does the Skool API support Skool's user mentions?

Yes. Posts and comments support the format [@Display Name](obj://user/{userId}). Get user IDs from members:list. This renders as a clickable mention in Skool, exactly like the UI.

Can I publish Skool courses programmatically?

Yes. The classroom:* namespace lets you create courses (top-level tiles), folders (sections), and pages (lessons). The classroom:setBody action takes markdown and converts it to Skool's TipTap JSON format internally — including headings, lists, code blocks, callouts (blockquotes), bold/italic/code, and tables. You can also attach downloadable resources via files:uploadFile + classroom:updateResources.

What happens if Skool changes their API?

The actor uses skool-js, a TypeScript library that is actively maintained. When Skool deploys a new version (~weekly), the buildId changes — the actor handles this automatically by refreshing from the homepage. WAF token expiration is also handled (auto-retry with re-auth). Breaking changes are documented in the What's new section of this README and the docs repo CHANGELOG.

Technical Details

  • Built on skool-js TypeScript library
  • Reads use Next.js SSR data endpoints (fast, no browser)
  • Writes use api2.skool.com REST endpoints
  • Playwright only for auth:login (cookie extraction)
  • Rate limiter: 60 reads/min, 30 writes/min
  • Auto-retry on transient errors with exponential backoff

Reliability — Why Apify's UNDER_MAINTENANCE flag doesn't apply here

Apify ships every actor through a daily Automated Quality Check: it runs the actor with the input schema's prefill/default values and expects SUCCEEDED + non-empty dataset + under 5 minutes. If the actor fails 2+ times in 3 days, Apify auto-flags it as UNDER_MAINTENANCE and every call returns x402-payment-required until the flag is cleared.

This actor is configured to always pass that check:

  • default action = system:health — no auth, no Skool calls, deterministic 1-item dataset, ~2s total. Apify's daily probe never fails because of credentials, rate-limits, or Skool downtime.
  • All errors are caught and pushed as structured {success:false} payloads. Runs always end SUCCEEDED regardless of outcome — Apify never sees an exit_fail from this actor.
  • system:health is also a one-call diagnostic for your own monitoring.

If you ever observe x402-payment-required, it's a transient Apify-side flag and will clear when the actor's owner runs PUT /v2/acts/{id} with {"notice": null}. Open an issue and I'll clear it within minutes.