Skip to the content.

Attach files to Skool lesson pages

When you publish a course on Skool, each lesson page can carry downloadable resources — a PDF cheatsheet, a workflow JSON, a spreadsheet template, an external link. The Skool admin UI lets you add these one at a time with a file picker. The API lets you do it programmatically, so you can attach 40 resources across 20 lessons in one script run.

This recipe is the second half of the “ship a course from Markdown” pipeline — once your course tree is built (folders + pages with body), this is how you attach the downloadable assets that turn lessons from “read-only content” into “templates the member can copy and use.”

Used in production to attach 23 Google Sheet templates (the canonical Cofre del Pirata T-xx pattern) across 4 courses in Cágala, Aprende, Repite.

Quick reference (TL;DR for agents)

   
Goal Add file or link resources to a Skool lesson page
Stack Any HTTP client + the Apify-hosted actor
Actions used files:uploadFileclassroom:updateResources
Setup time ~5 min
Ongoing cost $0.02 × N resources attached
Key gotcha #1 Use files:uploadFile (privacy: 1), NOT files:uploadImage (privacy: 0). Cover-image files are rejected as resources
Key gotcha #2 Each updateResources REPLACES the full array. To add 1 new, send the existing array + the new one
Title limit ~34 chars (truncated in UI past that)

The privacy-flag trap (read this first)

Skool’s POST /files accepts a privacy flag that determines bucket + ACL. Pick the wrong one and your file is unusable as a resource:

privacy Bucket Use case If you use it as resource
0 (default) sk-groups-dev-files (public) Cover images — public URL with .jpg suffix ❌ Rejected: 400 invalid file ... privacy: 0
1 sk-groups-files (private) Classroom Resources — signed URL on-demand ✅ Works

You can’t fix the privacy flag after upload. It’s set at the original POST /files and is not modifiable. If you accidentally uploaded with privacy 0 (e.g. used files:uploadImage), re-upload with files:uploadFile.

In the actor: files:uploadImage always sends privacy: 0 (for covers), files:uploadFile always sends privacy: 1 (for Resources). Use the right one.

Prerequisites

Step 1 — Upload the file (with privacy: 1)

{
  "action": "files:uploadFile",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "filePath": "/local/path/to/template.pdf",
    "fileName": "template.pdf",
    "contentType": "application/pdf"
  }
}

Response:

{
  "success": true,
  "file": {
    "id": "abc123...32hex",
    "file_name": "template.pdf",
    "content_type": "application/pdf"
  }
}

Save the file.id — you need it in step 2. The file is now in Skool’s private bucket. Direct GET to assets.skool.com/... returns 403 (correct: signed URLs are issued only when a member clicks Download from the page).

Step 2 — Attach as a resource

{
  "action": "classroom:updateResources",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "courseId": "course_32hex",
    "pageId": "page_32hex",
    "resources": [
      { "title": "Template (Google Sheet)", "file_id": "abc123..." }
    ]
  }
}

The actor handles the underlying call to PUT /courses/{pageId} with the canonical body shape (flat, update_resources: true, preserves title/desc/transcript/video_id via read-then-write).

Step 3 — Verify the resource appears

{
  "action": "classroom:getTree",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": { "groupSlug": "your-community" }
}

Navigate down to your page and check metadata.resources — it’s stored as a JSON-encoded string, so JSON.parse(metadata.resources) to read it programmatically. You should see your resource with auto-enriched file_name and file_content_type fields.

The 4 resource types Skool supports (visible in the admin “ADD” button): file, link, transcript, pin community post.

For a mix of file + external link in one update:

{
  "action": "classroom:updateResources",
  "cookies": "...",
  "groupSlug": "your-community",
  "params": {
    "courseId": "...",
    "pageId": "...",
    "resources": [
      { "title": "PDF Cheatsheet", "file_id": "abc123..." },
      { "title": "Sheet template (copy)", "link": "https://docs.google.com/spreadsheets/d/.../copy" }
    ]
  }
}

This is the Cofre del Pirata pattern: the file is the canonical Markdown wrapper (what/why/how), the link is the Google Sheet “make a copy” URL that the member opens to actually use the template. Both attached to the same lesson page.

Production gotchas

See also


Use this in production — no setup

The hardest part of building Skool automation isn’t the API logic — it’s the auth (cookies expire every ~3.5 days, WAF token rotation, weekly Skool buildId changes). The Skool All-in-One API actor on Apify handles all of that — including the read-then-write that preserves title/desc/video on every updateResources call.

→ Open the actor on Apify

New to Skool? Launch your community here — 14-day free trial.