Getting startedCarousels
Carousels
Upload image sets and publish ordered carousel posts.
Carousel workflow#
A carousel is 2–10 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 $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 $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 $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 $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 $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 18+ — run with HUMANPOST_API_KEY=... node carousels.mjs
import fs from "node:fs/promises";
import path from "node:path";
const API = "https://us-central1-clickbaitcarousel.cloudfunctions.net/publishingApi";
const key = process.env.HUMANPOST_API_KEY;
if (!key) throw new Error("Set HUMANPOST_API_KEY first");
const auth = { Authorization: `Bearer ${key}` };
const types = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
webp: "image/webp", heic: "image/heic" };
async function request(url, options) {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
return response.json();
}
const root = "./carousels";
const folders = (await fs.readdir(root, { withFileTypes: true }))
.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
for (const folder of folders) {
const dir = path.join(root, folder);
const images = (await fs.readdir(dir)).filter((file) =>
types[path.extname(file).slice(1).toLowerCase()]).sort();
if (images.length < 2 || images.length > 10)
throw new Error(`${folder} needs 2–10 supported images`);
// Upload and complete every image in filename order.
const uploadIds = [];
for (const filename of images) {
const file = path.join(dir, filename);
const contentType = types[path.extname(filename).slice(1).toLowerCase()];
const { size } = await fs.stat(file);
const { uploadId, uploadUrl } = await request(`${API}/v2/uploads`, {
method: "POST", headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ kind: "image", filename, contentType, sizeBytes: size }) });
const put = await fetch(uploadUrl, { method: "PUT",
headers: { "Content-Type": contentType }, body: await fs.readFile(file) });
if (!put.ok) throw new Error(`Upload failed: ${put.status}`);
await request(`${API}/v2/uploads/${uploadId}/complete`, { method: "POST", headers: auth });
uploadIds.push(uploadId);
}
// The stable key makes this safe to run again without duplicate posts.
await request(`${API}/v2/posts`, { method: "POST", headers: { ...auth,
"Content-Type": "application/json", "Idempotency-Key": `carousel-${folder}` },
body: JSON.stringify({ media: uploadIds, accountIds: ["ACCOUNT_ID"], caption: folder }) });
console.log(`Created: ${folder}`);
}Limits#
2–10 images per carousel
JPG, PNG, WebP, or HEIC
50 MB maximum per image
The media array sets slide order
Videos are single-file posts