ContentStudio's CLI
The ContentStudio CLI is a command-line tool that lets you control your entire ContentStudio workspace from the terminal. Instead of opening the dashboard, you can schedule posts, upload media, manage approvals, respond to comments, and audit your connected accounts โ all through simple commands. It connects directly to the same API your dashboard uses, so every action is reflected in real time. Whether you're running bash scripts, building CI/CD pipelines, or wiring up an AI agent, the CLI gives you full programmatic access to your ContentStudio workflow.
In this article
1. Installation
Install the CLI globally using npm or pnpm โ whichever you already use.
npm install -g contentstudio-cli # or if you prefer pnpm pnpm install -g contentstudio-cli
contentstudio --version contentstudio --help
npx skills add d4interactive/contentstudio-agentPick which agents to install into when prompted. After that, your agent will use the CLI without any extra prompting.
2. Authentication
Before running any commands, you need to authenticate with your ContentStudio API key.
Persist your key (recommended for local use)
cs_.
contentstudio auth:login --api-key cs_xxxxxxxxxxxxxxxxThis saves your key locally and verifies it immediately. You only need to do this once.
contentstudio auth:status # shows stored config (key is redacted) contentstudio --json auth:whoami # validates key against the API
3. Selecting a Workspace
Most commands are scoped to a workspace. Set your active workspace once and the CLI remembers it for all future commands.
contentstudio --json workspaces:listReturns workspace IDs, names, slugs, and timezones.
contentstudio workspaces:use <workspace_id>
contentstudio workspaces:current
Need to switch workspace for just one command? Use the --workspace flag inline โ it won't change your saved default:
contentstudio --workspace <other_workspace_id> accounts:list
4. Exploring Your Workspace
Before creating posts, you'll want to know which accounts, campaigns, labels, and team members are available in your workspace.
Connected social accounts
contentstudio --json accounts:list # all accounts contentstudio --json accounts:list --platform facebook # filter by platform contentstudio --json accounts:list --search "barcelona" # search by name
Valid --platform values: facebook, linkedin, twitter, instagram, youtube, tiktok, pinterest, gmb.
Campaigns, labels, categories, team members
contentstudio --json campaigns:list # folders / campaigns contentstudio --json categories:list # content categories contentstudio --json labels:list # labels contentstudio --json team:list # team members
All of these support --page, --per-page, and --search filters.
5. Connecting Social Accounts
There are three ways to connect new accounts, depending on the platform.
OAuth platforms (Facebook, LinkedIn, Twitter, Instagram, YouTube, TikTok, Pinterest, GMB, Threads, Tumblr)
contentstudio --json accounts:connect facebookThis returns a one-time
authorization_url.
To reconnect an account that has expired or gone invalid:
contentstudio --json accounts:connect facebook --reconnect --account-id <existing_account_id>
Available platform values: facebook, facebook-profile, instagram, instagram-via-facebook, twitter, linkedin, pinterest, tiktok, youtube, threads, gmb, tumblr.
Bluesky (no browser needed)
contentstudio --json accounts:add-bluesky \ --handle yourname.bsky.social \ --app-password xxxx-xxxx-xxxx-xxxx
- Use an app password, not your main password. Never pass your actual Bluesky account password here. App passwords can be revoked at any time from your settings.
- The CLI redacts the app password in
--dry-runoutput, but it is sent to ContentStudio's API over HTTPS during a live run.
Facebook Groups (manual)
contentstudio --json accounts:add-facebook-group \ --name "My Community Group" \ --image https://example.com/group-cover.jpg # image URL is optional
All three connect methods support --dry-run to preview the payload without calling the API.
6. Creating Posts
There are two ways to create a post โ shortcut flags for simple cases, or a full JSON body for platform-specific options.
Shortcut flags (simple posts)
# Scheduled post to a Facebook page with one image contentstudio posts:create \ -c "Our latest blog post is live!" \ -i <account_id> \ -t scheduled \ -s "2026-05-01 10:00:00" \ -m https://example.com/hero.jpg
| Flag | What it does |
|---|---|
-c, --content |
The post text / caption |
-i, --account |
Social account ID. Repeat for multi-account posts. |
-t, --publish-type |
scheduled | draft | queued | content_category |
-s, --scheduled-at |
Schedule time in YYYY-MM-DD HH:MM:SS format |
-m, --image-url |
External image URL. Repeatable for multiple images. |
--video-url |
External video URL |
--media-id |
ID of an asset already in your media library. Repeatable. |
--post-type |
feed | reel | story | feed+reel | feed+story | carousel | video | shorts |
--dry-run |
Print the payload and exit โ no API call made |
Posting to multiple accounts at once
Repeat the -i flag for each account:
contentstudio posts:create \ -c "Cross-platform announcement ๐" \ -i <facebook_id> \ -i <linkedin_id> \ -i <twitter_id> \ -t scheduled \ -s "2026-05-01 09:00:00"
Using media from your library
# Find a media ID contentstudio --json media:list --type images # Reference it by ID in your post contentstudio posts:create \ -c "Post with library asset" \ -i <account_id> \ -t draft \ --media-id <media_library_id>
Full body via JSON file (for advanced / platform-specific options)
For TikTok privacy settings, YouTube categories, GMB topic types, approval workflows, first comments, labels, and campaigns โ write a JSON body file and pass it with --body:
contentstudio --json posts:create --body /tmp/post.json
Full JSON schema:
{
"content": {
"text": "Hello world",
"media": {
"images": ["https://example.com/img.jpg"],
"video": "https://example.com/clip.mp4",
"media_ids": ["<media_library_id>"]
}
},
"accounts": ["<account_id>"],
"post_type": "reel+story",
"post_video_title": "My Video Title",
"scheduling": {
"publish_type": "scheduled",
"scheduled_at": "2026-05-01 10:00:00"
},
"first_comment": {
"message": "๐ link in bio",
"accounts": ["<account_id>"]
},
"labels": ["<label_id>"],
"campaign_id": "<campaign_id>",
"approval": {
"approvers": ["<user_id>"],
"approve_option": "anyone",
"notes": "please review"
},
"youtube_options": { "title": "...", "privacy_status": "public", "category": "EDUCATION", "tags": ["tag1"], "license": "youtube", "made_for_kids": false },
"tiktok_options": { "privacy_level": "PUBLIC_TO_EVERYONE", "disable_comment": false, "disable_duet": false, "disable_stitch": false, "auto_add_music": false },
"pinterest_options": { "title": "...", "link": "https://..." },
"gmb_options": { "topic_type": "EVENT", "start_date": "2026-05-01", "end_date": "2026-05-02", "title": "...", "action_type": "BOOK", "cta_link": "https://..." }
}
--dry-run โ it prints the full request body and exits without touching the API. Use it before creating, deleting, or approving anything for the first time:
contentstudio --json posts:create --dry-run \ -c "Test" -i <account_id> -t scheduled -s "2026-05-01 10:00"
7. Platform-Specific Examples
contentstudio --json posts:create \ -c "Big news for our community ๐" \ -i <facebook_page_id> \ -t scheduled \ -s "2026-05-01 10:00:00" \ -m https://example.com/announcement.jpg # Facebook Reels or Stories โ add --post-type contentstudio posts:create \ -c "Behind-the-scenes" \ -i <facebook_id> \ -t scheduled \ -s "2026-05-01 10:00:00" \ --video-url https://example.com/clip.mp4 \ --post-type reel+story
contentstudio --json posts:create \ -c "Excited to share our Q2 roadmap" \ -i <linkedin_id> \ -t scheduled \ -s "2026-05-01 09:00:00" \ -m https://example.com/roadmap.png
Twitter / X
contentstudio --json posts:create \ -c "New release shipped ๐" \ -i <twitter_id> \ -t scheduled \ -s "2026-05-01 10:00:00" \ -m https://example.com/preview.png
Instagram (feed, reel, story)
# Feed post contentstudio posts:create \ -c "Caption with #hashtags" \ -i <instagram_id> \ -t scheduled -s "2026-05-01 10:00:00" \ -m https://example.com/photo.jpg \ --post-type feed # Reel contentstudio posts:create \ -c "" \ -i <instagram_id> \ -t scheduled -s "2026-05-01 10:00:00" \ --video-url https://example.com/reel.mp4 \ --post-type reel # Story contentstudio posts:create \ -c "" \ -i <instagram_id> \ -t scheduled -s "2026-05-01 10:00:00" \ -m https://example.com/story.jpg \ --post-type story
YouTube (Videos & Shorts)
YouTube requires youtube_options โ use a JSON body file:
cat > /tmp/yt-post.json <<'JSON'
{
"content": {
"text": "Description shown under the video",
"media": {"video": "https://example.com/clip.mp4"}
},
"accounts": ["<youtube_id>"],
"post_type": "shorts",
"post_video_title": "How we built ContentStudio CLI",
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"youtube_options": {
"title": "How we built ContentStudio CLI",
"privacy_status": "public",
"category": "EDUCATION",
"tags": ["cli", "automation", "social-media"],
"license": "youtube",
"made_for_kids": false
}
}
JSON
contentstudio --json posts:create --body /tmp/yt-post.json
TikTok
cat > /tmp/tt-post.json <<'JSON'
{
"content": {
"text": "Quick demo #fyp #tutorial",
"media": {"video": "https://example.com/tiktok.mp4"}
},
"accounts": ["<tiktok_id>"],
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"tiktok_options": {
"privacy_level": "PUBLIC_TO_EVERYONE",
"disable_comment": false,
"disable_duet": false,
"disable_stitch": false,
"auto_add_music": false,
"brand_content_toggle": false,
"disclose_commercial_content": false,
"is_aigc": false
}
}
JSON
contentstudio --json posts:create --body /tmp/tt-post.json
cat > /tmp/pin-post.json <<'JSON'
{
"content": {
"text": "Check out our spring guide",
"media": {"images": ["https://example.com/pin.jpg"]}
},
"accounts": ["<pinterest_id>"],
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"pinterest_options": {
"title": "Spring 2026 Style Guide",
"link": "https://example.com/spring-guide"
}
}
JSON
contentstudio --json posts:create --body /tmp/pin-post.json
Google Business Profile
cat > /tmp/gmb-post.json <<'JSON'
{
"content": {
"text": "Join our grand opening event",
"media": {"images": ["https://example.com/event.jpg"]}
},
"accounts": ["<gmb_account_id>"],
"scheduling": {"publish_type": "scheduled", "scheduled_at": "2026-05-01 10:00:00"},
"gmb_options": {
"topic_type": "EVENT",
"start_date": "2026-05-15",
"end_date": "2026-05-16",
"title": "Grand Opening",
"action_type": "BOOK",
"cta_link": "https://example.com/rsvp"
}
}
JSON
contentstudio --json posts:create --body /tmp/gmb-post.json
8. Managing Posts
Listing posts
contentstudio --json posts:list # all recent contentstudio --json posts:list --status draft --per-page 5 contentstudio --json posts:list --status scheduled --status published contentstudio --json posts:list --date-from 2026-04-01 --date-to 2026-04-30
Deleting a post
# Delete from ContentStudio only contentstudio --json posts:delete <post_id> # Also delete from the connected social platforms contentstudio --json posts:delete <post_id> --delete-from-social # Limit cross-platform delete to a specific account contentstudio --json posts:delete <post_id> --account <account_id> --delete-from-social # Preview without deleting contentstudio --json posts:delete <post_id> --dry-run
Approving or rejecting posts
contentstudio --json posts:approve <post_id> --comment "LGTM, ship it" contentstudio --json posts:reject <post_id> --comment "fix the link first" # Preview without acting contentstudio --json posts:approve <post_id> --dry-run
9. Comments & Internal Notes
# List all comments and notes on a post contentstudio --json comments:list <post_id> # Add a public comment contentstudio --json comments:add <post_id> "Great work team!" # Add an internal note (not visible publicly) contentstudio --json comments:add <post_id> "Double-check the link before publishing" --note # Mention team members contentstudio --json comments:add <post_id> "Heads up" --mention <user_id> --mention <user_id> # Preview without posting contentstudio --json comments:add <post_id> "test" --note --dry-run
10. Media Library
Listing assets
contentstudio --json media:list # all assets contentstudio --json media:list --type images --sort recent --per-page 20 contentstudio --json media:list --type videos contentstudio --json media:list --search "campaign-2026"
Valid --sort values: recent, oldest, size, a2z, z2a.
Uploading assets
# Upload a local file contentstudio --json media:upload --file ./hero.jpg # Import from an external URL contentstudio --json media:upload --url https://example.com/asset.mp4 # Optionally put it in a specific folder contentstudio --json media:upload --file ./hero.jpg --folder-id <folder_id> # Preview without uploading contentstudio --json media:upload --url https://example.com/img.jpg --dry-run
The upload response includes an _id you can pass as --media-id when creating posts.
11. Automation Workflows
Copy-paste ready scripts for common scenarios.
Schedule a daily post for the next 7 days
#!/bin/bash
ACCOUNT="<facebook_page_id>"
CONTENT=(
"Monday motivation ๐ช"
"Tuesday tips: keep it simple"
"Wednesday wisdom from the team"
"Throwback Thursday"
"Friday vibes ๐"
"Weekend prep โ try this"
"Sunday reflections"
)
for i in "${!CONTENT[@]}"; do
DATE=$(date -d "+$((i+1)) day 09:00" '+%F %T')
contentstudio --json posts:create \
-c "${CONTENT[$i]}" \
-i "$ACCOUNT" \
-t scheduled \
-s "$DATE"
done
Cross-platform campaign (same post, multiple platforms)
#!/bin/bash TIME="2026-05-01 10:00:00" FB=$(contentstudio --json accounts:list --platform facebook | jq -r '.data[0]._id') LI=$(contentstudio --json accounts:list --platform linkedin | jq -r '.data[0]._id') TW=$(contentstudio --json accounts:list --platform twitter | jq -r '.data[0]._id') contentstudio --json posts:create \ -c "Big launch today ๐" \ -i "$FB" -i "$LI" -i "$TW" \ -t scheduled \ -s "$TIME" \ -m https://example.com/launch.jpg
Bulk-delete drafts older than 30 days
#!/bin/bash
CUTOFF=$(date -d '-30 days' '+%Y-%m-%d')
contentstudio --json posts:list --status draft --date-to "$CUTOFF" --per-page 100 \
| jq -r '.data[]._id' \
| while read id; do
contentstudio --json posts:delete "$id"
done
Upload a folder of images, one post per image
#!/bin/bash
ACCOUNT="<instagram_id>"
for img in ./photos/*.jpg; do
RESP=$(contentstudio --json media:upload --file "$img")
MEDIA_ID=$(echo "$RESP" | jq -r '.data._id')
TIME=$(date -d "+1 hour" '+%F %T')
contentstudio --json posts:create \
-c "$(basename "$img" .jpg)" \
-i "$ACCOUNT" \
-t scheduled \
-s "$TIME" \
--media-id "$MEDIA_ID" \
--post-type feed
done
Auto-approve posts from a trusted creator
#!/bin/bash
TRUSTED_USER_ID="<user_id>"
contentstudio --json posts:list --status pending_approval --per-page 50 \
| jq -r --arg u "$TRUSTED_USER_ID" '.data[] | select(.created_by == $u) | ._id' \
| while read id; do
contentstudio --json posts:approve "$id" --comment "auto-approved (trusted creator)"
done
12. JSON Output (for scripts & agents)
Every command supports --json. The response always has the same shape, making it easy to pipe into jq or handle in scripts.
// Success
{ "ok": true, "data": <payload> }
// Error
{
"ok": false,
"error": {
"type": "AuthError",
"message": "Invalid or revoked API key",
"http_status": 401,
"hint": "Run `contentstudio auth:login --api-key cs_...` to set a valid API key."
}
}
Always check both ok and the process exit code (non-zero on error).
13. Error Reference
The CLI uses typed errors with specific exit codes so your scripts can handle failures cleanly.
| Exit code | Error type | HTTP | What it means |
|---|---|---|---|
1 |
ContentStudioError |
varies | Generic error โ check the message |
2 |
AuthError |
401, 403 | Invalid or revoked key โ run auth:login again |
3 |
NotFoundError |
404 | Resource doesn't exist or wrong workspace |
4 |
ValidationError |
422 | Malformed request โ check the message for field details |
5 |
RateLimitError |
429 | Too many calls โ back off and retry |
6 |
BackendError |
5xx / network | Upstream issue โ retry with backoff |
1 |
ConfigError |
โ | Local config issue (no key or workspace set) โ see hint in output |
14. Quick Reference
# โโ Authentication โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio auth:login --api-key cs_... # Persist key + verify contentstudio auth:status # Show local config contentstudio --json auth:whoami # Validate against API contentstudio auth:logout # Forget credentials # โโ Workspaces โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio --json workspaces:list # List all workspaces contentstudio workspaces:use <workspace_id> # Set active workspace contentstudio workspaces:current # Show active workspace # โโ Discovery โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio --json accounts:list [--platform] [--search] contentstudio --json campaigns:list contentstudio --json categories:list contentstudio --json labels:list contentstudio --json team:list # โโ Posts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio --json posts:list [--status] [--date-from] [--date-to] contentstudio --json posts:create -c "text" -i <id> -t draft contentstudio --json posts:create --body /path/to/post.json contentstudio --json posts:create [...] --dry-run # preview only contentstudio --json posts:delete <post_id> [--delete-from-social] contentstudio --json posts:approve <post_id> [--comment "..."] contentstudio --json posts:reject <post_id> [--comment "..."] # โโ Comments / Notes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio --json comments:list <post_id> contentstudio --json comments:add <post_id> "message" [--note] [--mention <id>] # โโ Media โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio --json media:list [--type images|videos] [--sort recent] contentstudio --json media:upload --file <path> contentstudio --json media:upload --url <url> # โโ Global flags โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ contentstudio --version contentstudio --help contentstudio <command> --help contentstudio --json ... # JSON envelope output contentstudio --workspace <id> ... # Per-call workspace override contentstudio --base-url <url> ... # Per-call API URL override
15. API Endpoints
The CLI wraps these 20 endpoints from the ContentStudio v1 public API. Base URL: https://api.contentstudio.io/api/v1
| Method | Endpoint | CLI command |
|---|---|---|
| GET | /me |
auth:whoami |
| GET | /platforms |
platforms:list |
| GET | /workspaces |
workspaces:list |
| GET | /workspaces/{w}/accounts |
accounts:list |
| POST | /workspaces/{w}/connect/{platform} |
accounts:connect <platform> |
| POST | /workspaces/{w}/add/bluesky |
accounts:add-bluesky |
| POST | /workspaces/{w}/add/facebook-group |
accounts:add-facebook-group |
| GET | /workspaces/{w}/campaigns |
campaigns:list |
| GET | /workspaces/{w}/content-categories |
categories:list |
| GET | /workspaces/{w}/labels |
labels:list |
| GET | /workspaces/{w}/team-members |
team:list |
| GET | /workspaces/{w}/media |
media:list |
| POST | /workspaces/{w}/media |
media:upload |
| GET | /workspaces/{w}/posts |
posts:list |
| POST | /workspaces/{w}/posts |
posts:create |
| DELETE | /workspaces/{w}/posts/{p} |
posts:delete |
| POST | /workspaces/{w}/posts/{p}/approval |
posts:approve, posts:reject |
| GET | /workspaces/{w}/posts/{p}/comments |
comments:list |
| POST | /workspaces/{w}/posts/{p}/comments |
comments:add |
Full OpenAPI spec: api.contentstudio.io/api-docs.json ยท Human-readable docs: api.contentstudio.io/guide ยท npm: contentstudio-cli ยท GitHub: d4interactive/contentstudio-agent
FAQs
- Do I need to log in every time I use the CLI? No. Running
contentstudio auth:login --api-key cs_...saves your key locally, so you only need to do it once. You can check your stored credentials anytime withcontentstudio auth:status. - Can I post to multiple social accounts at the same time? Yes. Just repeat the
-iflag for each account ID in yourposts:createcommand. One API call, one post, multiple platforms. - What's the difference between
--image-urland--media-id?--image-urlpulls in an image from an external URL on the fly.--media-idreferences an asset you've already uploaded to your ContentStudio media library. Usemedia:listto find existing asset IDs. - How do I preview a command without actually sending anything? Add
--dry-runto any mutating command. It prints the full request payload and exits without touching the API. This works forposts:create,posts:delete,posts:approve, and more. - When do I need a JSON body file instead of shortcut flags? Whenever you need platform-specific options โ like TikTok privacy settings, YouTube categories, Pinterest link URLs, or Google Business event details. Pass the file with
--body /path/to/post.json. - What happens if I hit a rate limit? The CLI handles it automatically. It retries up to twice with exponential backoff on 429 and 5xx errors, so you don't need to build retry logic yourself.
- Can I scope a command to a different workspace without changing my default? Yes. Use the
--workspace <id>flag inline on any command. It overrides the active workspace for that call only and leaves your saved default untouched. - How do I connect a Bluesky account? Bluesky doesn't use OAuth. Generate an app password at bsky.app under Settings โ App Passwords, then run
accounts:add-blueskywith your handle and that app password. Never use your main Bluesky password here. - Can I use the CLI in automated scripts? Yes, it's designed for this. Use
--jsonon any command to get a consistent{ "ok": true, "data": ... }envelope you can pipe intojqor handle in bash. Non-zero exit codes signal errors, so standard script error handling works out of the box. - Why is my command returning a NotFoundError? Usually this means the resource ID doesn't exist in your currently active workspace. Double-check which workspace is active with
workspaces:current, and make sure the post, account, or asset ID belongs to it.