Edit published Skool posts via API
Need to fix a typo in a pinned post, swap a stale link across 20 announcements, or update the title of an evergreen feed post after a rebrand? The Skool UI lets you edit one post at a time. The API does the same write, scriptable — so you can run a corrections pass over hundreds of posts in one go.
The catch: the posts:update action has one silent-fail gotcha that wasted half a day of debugging when we first hit it. This recipe shows the canonical shape and the read-then-write + verify-fetch pattern that makes updates reliable.
Quick reference (TL;DR for agents)
| Goal | Update title, content, or label of an existing Skool post |
| Stack | Any HTTP client + the Apify-hosted actor |
| Actions used | posts:get → posts:update → posts:get (verify) |
| Setup time | ~5 min |
| Ongoing cost | $0.01 × N posts updated |
| Key gotcha | Body MUST be flat (no metadata wrapper) or Skool returns 200 OK and silently ignores the update |
| Mandatory pattern | write + verify-fetch — never trust the 200 |
Prerequisites
- Apify token (get one)
- Skool admin cookies for the community (see Authentication)
- The
postIdof each post you want to edit (32-char hex; visible in the URL as?p={shortId}— useposts:filterto map shortIds to full postIds)
The non-obvious thing about posts:update
There are two body shapes that LOOK valid to a developer reading the network tab, but only one actually works:
// ❌ THIS GETS 200 OK BUT THE UPDATE IS SILENTLY IGNORED
{
"post_type": "generic",
"group_id": "...",
"metadata": {
"title": "New title",
"content": "New body"
}
}
// ✅ THIS WORKS — flat body, no wrapper
{
"title": "New title",
"content": "New body",
"attachments": "",
"labels": "existing_label_id_or_empty",
"video_links": "",
"video_ids": []
}
We caught this by capturing the Skool admin UI’s own update call with API Reverse Engineer — the UI sends the flat shape, and that’s the only one Skool actually persists. The metadata-wrapped shape returns a clean 200 OK response but the post in the database doesn’t change.
The actor (posts:update) sends the correct flat shape internally. You only see this gotcha if you’re calling the Skool API directly without the wrapper.
Step 1 — Read the current post (to preserve labels)
If the post has a category label assigned, Skool requires labels in the update body or returns 400 "one or more labels required". Always read first:
{
"action": "posts:get",
"cookies": "...",
"groupSlug": "your-community",
"params": { "postId": "abc123...32hex" }
}
Save labelId (or metadata.labels) for the update call. If the post has no label, this can be empty.
Step 2 — Update
{
"action": "posts:update",
"cookies": "...",
"groupSlug": "your-community",
"params": {
"postId": "abc123...32hex",
"title": "New title here",
"content": "New body content as plain text. Mentions use [@Name](obj://user/{userId}) syntax.",
"labels": "preserve_the_labelId_from_step_1"
}
}
Field rules:
| Field | Rules |
|---|---|
title |
String. Empty string clears the title. |
content |
Plain text only. No TipTap, no Markdown — posts use raw text. Linebreaks are \n. |
labels |
Pass the existing labelId to preserve, or empty string if post had none. Required if post had a label. |
video_ids |
Must be array [], never string "" — passing string returns 500. |
Step 3 — Verify (never trust the 200)
{ "action": "posts:get", "cookies": "...", "groupSlug": "your-community", "params": { "postId": "..." } }
Assert that title and content match what you sent. This step is non-negotiable. Skool’s write endpoints have a documented history of returning success while silently no-op’ing — the only proof an update actually applied is a read-back showing the new value.
If verify fails: most likely you sent the wrapped metadata shape (you’re calling the API directly, not the actor) or you missed labels for a labelled post.
Production patterns
Bulk find-and-replace across the feed. List all posts containing a stale URL, run the swap, verify each.
# pseudo-flow
posts:filter --query "old-url.com" → for each: posts:get → posts:update (replace string) → posts:get (verify)
Edit a pinned announcement. Pinned posts in the feed get the most eyeballs. The API lets you update them without unpinning (avoiding the “new post” notification spam that re-pinning triggers).
Correct typos in onboarding pinned threads. The “Start here” post in Cágala, Aprende, Repite’s feed is updated this way when steps change — no notification fired, content updates in place.
Production gotchas
- The silent-fail gotcha (above). Wrapped body → 200 OK → no change in DB. The actor handles this. If you’re hitting Skool directly, copy the flat shape exactly.
labelsis required if the post had one. Read first, preserve, write. Otherwise400.video_ids: ""(string) → 500. Must be[](array). The actor enforces this.parseRawPostskool-js bug: the library mapsp.labelId(camelCase) but Skool returnslabel_id(snake_case). If you’re using skool-js directly, fetch the raw response and readlabel_idormetadata.labelsmanually. The actor’sposts:updatealready handles this.- Comments are preserved on update. Editing a post doesn’t wipe its comment thread.
- No mention re-notification. Adding a
[@Name]mention in an edit does NOT trigger a notification (Skool only fires the first time the mention appears).
See also
- Posts API reference — every post action with params and gotchas
- Recipe: Reply to unanswered posts — complementary write-path for posts
- Posts and the silent-fail pattern — why write+verify-fetch is the rule for everything
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 flat-body shape for posts:update.
- Pay-per-event pricing (~$1.50/mo for typical communities)
- One JSON POST per action — works from any HTTP client
- The silent-fail wrapper bug is wrapped inside the action — you get the right shape by default
New to Skool? Launch your community here — 14-day free trial.