Publishing guidesPublishing carousels
Publishing carousels
Upload image sets and publish ordered carousel posts.
Carousel workflow#
A carousel is 2–20 images published as one post. For local files, create an upload for each image, send the file to its temporary upload URL within 15 minutes, and mark it complete. Then create one post with the upload IDs in slide order. For a public image URL, send kind and sourceUrl instead; HumanPost imports and completes it automatically.
One carousel with curl#
# 1. Create an upload for slide 1
SIZE=$(wc -c < slide-1.jpg | tr -d ' ')
UPLOAD=$(curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads" \
-H "Authorization: Bearer $HUMANPOST_API_KEY" -H "Content-Type: application/json" \
-d "{\"kind\":\"image\",\"filename\":\"slide-1.jpg\",\"contentType\":\"image/jpeg\",\"sizeBytes\":$SIZE}")
UPLOAD_ID_1=$(printf '%s' "$UPLOAD" | jq -r '.uploadId')
UPLOAD_URL=$(printf '%s' "$UPLOAD" | jq -r '.uploadUrl')
# 2. PUT the raw file, then mark the upload complete
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --upload-file slide-1.jpg
curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads/$UPLOAD_ID_1/complete" \
-H "Authorization: Bearer $HUMANPOST_API_KEY"
# 3. Repeat for slide 2
SIZE=$(wc -c < slide-2.jpg | tr -d ' ')
UPLOAD=$(curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads" \
-H "Authorization: Bearer $HUMANPOST_API_KEY" -H "Content-Type: application/json" \
-d "{\"kind\":\"image\",\"filename\":\"slide-2.jpg\",\"contentType\":\"image/jpeg\",\"sizeBytes\":$SIZE}")
UPLOAD_ID_2=$(printf '%s' "$UPLOAD" | jq -r '.uploadId')
UPLOAD_URL=$(printf '%s' "$UPLOAD" | jq -r '.uploadUrl')
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --upload-file slide-2.jpg
curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/uploads/$UPLOAD_ID_2/complete" \
-H "Authorization: Bearer $HUMANPOST_API_KEY"
# 4. Create one post; media array order is slide order
curl -s -X POST "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi/v2/posts" \
-H "Authorization: Bearer $HUMANPOST_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: two-slide-launch" \
-d "{\"media\":[\"$UPLOAD_ID_1\",\"$UPLOAD_ID_2\"],\"accountIds\":[\"ACCOUNT_ID\"],\"caption\":\"Two ideas worth saving\"}"Bulk Node script#
Have a folder of carousels? Each subfolder becomes one post. Save this as carousels.mjs. Images are sorted by filename, and each folder name becomes its post caption.
// Node.js 20+; install the CLI first: npm install -g ./cli
// Set HUMANPOST_API_KEY through your shell or secrets manager.
import fs from "node:fs/promises";
import path from "node:path";
import { execFileSync } from "node:child_process";
const env = { ...process.env, HUMANPOST_API_BASE: "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi" };
if (!env.HUMANPOST_API_KEY) throw new Error("Set HUMANPOST_API_KEY first");
const root = "./carousels";
const folders = (await fs.readdir(root, { withFileTypes: true }))
.filter(entry => entry.isDirectory()).map(entry => entry.name).sort();
function cli(args) {
return JSON.parse(execFileSync("humanpost", [...args, "--json"],
{ env, encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] }));
}
for (const folder of folders) {
const dir = path.join(root, folder);
const files = (await fs.readdir(dir)).filter(file => /\.(jpe?g|png|webp|heic)$/i.test(file)).sort();
if (files.length < 2 || files.length > 20) throw new Error(folder + " needs 2–20 images");
const manifest = path.join(dir, ".upload-manifest.json");
const state = path.join(dir, ".upload-progress.json");
await fs.writeFile(manifest, JSON.stringify(files.map(file => path.join(dir, file))));
// Replays reuse ready upload IDs; changed files/order are rejected by the state check.
const uploads = cli(["uploads", "local-batch", "--file", manifest, "--state", state]);
const post = cli(["posts", "create", "--idempotency-key", "carousel-" + folder,
"--data", JSON.stringify({ media: uploads.results.map(item => item.uploadId),
accountIds: ["ACCOUNT_ID"], caption: folder })]);
console.log(folder + ": " + post.postId);
}Limits#
2–20 images per carousel
JPG, PNG, WebP, or HEIC
50 MB maximum per image
The media array sets slide order
Videos are single-file posts