Skip to Content

PostNitro CLI

The PostNitro CLI creates on-brand social media posts — multi-slide carousels, single images, and short videos — and schedules them to LinkedIn, Instagram, TikTok, and Threads, all from the command line. It’s a thin, scriptable wrapper over the Embed API, built for AI agents, automations, and scripts: every command takes flags or JSON in and prints JSON out (clean stdout on success, JSON error on stderr) — no colors, tables, or interactive prompts to parse around. It uses the same Embed API key and credits as the REST API and MCP server.

Quick Reference

Package@postnitro/cli (npm)
Binarypostnitro
RequiresNode.js ≥ 18, and a PostNitro account on a paid plan
Authentication--api-key, POSTNITRO_API_KEY, or a saved key — keys start with pn-
API keySame key as the Embed API — how to get one
OutputJSON to stdout on success (exit 0); JSON error to stderr (exit 1)
GitHubpostnitro/postnitro-agent 

Installation

# Install globally from npm (recommended) npm install -g @postnitro/cli postnitro --help # Or run without installing npx @postnitro/cli --help

Install as a skill

You can also install PostNitro as a skill for your AI coding agent — it teaches the agent to drive the CLI on your behalf:

npx skills add postnitro/postnitro-agent

For Claude Code specifically, you can instead add it as a plugin:

# in Claude Code /plugin marketplace add postnitro/postnitro-agent /plugin install postnitro@postnitro-agent

The skill is defined in SKILL.md at the repo root  (with a copy under skills/postnitro/); the Claude Code plugin manifests live in .claude-plugin/.

Authentication

Get an API key from PostNitro → Profile → Embed → Generate API Key (see Obtaining an API Key). Provide it in any of these ways (highest precedence first):

  1. --api-key <key> on any command
  2. POSTNITRO_API_KEY environment variable
  3. Saved config — postnitro auth set-key <key> (stored at ~/.postnitro-cli/config.json)
postnitro auth set-key pn-xxxxxxxxxxxx # save it once postnitro auth status # check what's configured (key is masked) postnitro auth clear # remove the saved key

Security: auth set-key stores your key in plaintext at ~/.postnitro-cli/config.json. Restrict its permissions, avoid shared machines, and use --api-key or POSTNITRO_API_KEY in CI.

Core concepts

Post type × content source. Creating a post is two independent choices:

generate (AI writes it)import (you supply content)
carousel (multi-slide)postnitro carousel generatepostnitro carousel import (slides array)
image (single)postnitro image generatepostnitro image import (single slide object)

Asynchronous by default. Creating a post kicks off a background job and returns an embedPostId. Add --wait to poll to completion and get the final output in one call, or poll manually with status / output.

embedPostId vs designId. Creation returns an embedPostId (the job). To schedule a post you need its designId (from the --wait result or output). Never pass an embedPostId to schedule — see carousel identifiers.

Editor link. Every completed post’s output includes an editorUrl — a deep link to open the design in the PostNitro editor.

JSON in / JSON out. Complex inputs can be passed inline as JSON (agent-friendly) or from a file; inline always wins. Every command prints JSON to stdout on success (exit 0) or a JSON error to stderr (exit 1).

Smart defaults. --template-id / --brand-id / --preset-id resolve in this order: explicit flag → saved default (defaults set) → auto-select when your workspace has exactly one candidate.

Global options

Available on every command:

OptionDescription
--api-key <key>PostNitro API key (falls back to POSTNITRO_API_KEY, then saved config)
-V, --versionPrint the CLI version
-h, --helpShow help for any command or subcommand

Command reference

auth

Manage the saved PostNitro API key.

CommandDescription
auth set-key <apiKey>Save an API key to ~/.postnitro-cli/config.json
auth statusShow whether a key is configured (masked) and where it’s coming from
auth clearRemove the saved API key

defaults

Save default templateId / brandId / presetId / responseType so create commands don’t need them every time.

CommandOptionsDescription
defaults getShow saved defaults for the active API key
defaults set--template-id <id> --brand-id <id> --preset-id <id> --response-type <PDF|PNG|DESIGN>Save one or more defaults (at least one required)
postnitro defaults set --template-id tpl_123 --brand-id brd_456 --preset-id prs_789 --response-type PDF postnitro defaults get

template

Browse available carousel templates.

CommandOptionsDescription
template list--page <n> (default 1) · --limit <n> (default 10, max 50)List templates with IDs, names, and dimensions
postnitro template list --limit 20

brand

Manage brand kits (logo, name, handle stamped on designs). See the Brands API for full field definitions.

CommandDescription
brand list [--page <n>] [--limit <n>]List brand kits
brand get <id>Fetch a single brand kit
brand create [options]Create a brand kit (required: --name, --handle, --image, or --data/--file)
brand update <id> [options]Update a brand kit (all fields required by the API — fetch current values first)

Options for create / update:

OptionDescription
--name <name>Brand display name
--handle <handle>Social handle, e.g. @postnitroai
--image <url>Logo image URL
--company-detailTreat as a company brand (vs personal)
--data <json>Full brand object as inline JSON (overrides --file and the flags)
--file <path>Full brand object from a JSON file (overridden by --data)
--no-show-name / --no-show-handle / --no-show-imageHide that element on slides
postnitro brand create --name "PostNitro" --handle "@postnitroai" --image "https://.../logo.png" --company-detail postnitro brand create --data '{"name":"PostNitro","handle":"@postnitroai","image":"https://.../logo.png","isCompanyDetail":true}'

preset

Browse available AI generation presets (each bundles social platform, tone, audience, language, slide count, and model).

CommandOptionsDescription
preset list--page <n> · --limit <n> (max 50)List AI presets with IDs

social

Manage connected social accounts. See the Social Accounts API for full field definitions.

CommandDescription
social listList connected accounts, grouped by platform (returns the IDs used for --selected-accounts)
social get <id>Fetch a single account, including usage stats
social disconnect <id> --yesDisconnect an account. Destructive, requires --yes, cannot be undone

Generate, import, and inspect multi-slide carousel posts.

CommandDescription
carousel import-templatePrint the exact slide structure and rules for carousel import
carousel generate [options]Generate a carousel with AI
carousel import [options]Create a carousel from your own slides
carousel status <embedPostId>Check generation status + processing logs
carousel output <embedPostId>Retrieve the final output (files, designId, editorUrl)

carousel generate options:

OptionDescription
--context <text>Required. Topic/prompt, or an article/X URL (depending on --type)
--type <text|article|x>How --context is interpreted (default text)
--instructions <text>Extra guidance for the AI
--template-id <id> / --brand-id <id> / --preset-id <id>Override saved defaults
--response-type <PDF|PNG|DESIGN>Output format (default PDF) — see Response types
--requestor-id <id>Optional custom tracking ID
--waitPoll until complete and print the final output
--generate-images + --image-context + --image-placement + --image-strategyAI image generation

carousel import options: same as above except no --type / --instructions / --preset-id, plus:

OptionDescription
--slides <json>Slides as inline JSON — a bare array or {"slides":[...]} (overrides --file)
--file <path>Path to a JSON file containing the slides array
# AI-generated postnitro carousel generate --context "5 tips for remote work" --type text --wait postnitro carousel generate --context "https://example.com/post" --type article --wait # Import your own slides postnitro carousel import --slides '{"slides":[ {"type":"starting_slide","heading":"Your Title","description":"Intro"}, {"type":"body_slide","heading":"Key Point","description":"Details"}, {"type":"ending_slide","heading":"Take Action!","cta_button":"Learn More"} ]}' --wait

Carousel slide rules: the array must contain exactly 1 starting_slide (first), ≥1 body_slide, and exactly 1 ending_slide (last). Every slide needs a heading. Optional per slide: sub_heading, description, image, background_image, cta_button, and layoutType/layoutConfig (see Infographic layout). Run carousel import-template for the authoritative schema.

image

Generate, import, and inspect single-image posts. Mirrors carousel, but produces one image and import takes a single slide object (not an array).

CommandDescription
image import-templatePrint the exact slide object and rules for image import
image generate [options]Generate a single image with AI (same options as carousel generate)
image import [options]Create an image from your own content
image status <embedPostId>Check generation status + logs
image output <embedPostId>Retrieve the final output

image import options: same as carousel import, but slide input is a single object:

OptionDescription
--slide <json>The slide as inline JSON — a single object (overrides --file)
--file <path>Path to a JSON file with a single slide object (or { "slides": {...} })
postnitro image generate --context "Announce our new scheduling feature" --wait postnitro image import --slide '{"heading":"Welcome!","sub_heading":"Subtitle","cta_button":"Learn more"}' --wait

Image slide fields (single object; heading required, all others optional): heading, sub_heading, description, cta_button, image, background_image, plus layoutType/layoutConfig for infographics. Any other field is rejected. Sending an array is rejected — arrays are carousel-only.

video

Generate, import, and inspect video posts. Slides are scenes, so video import takes the same slide array as carousel import — see Video posts for the API-level rules.

CommandDescription
video import-templatePrint the scene rules and render settings for video import
video generate [options]Generate a video with AI (each AI-written slide becomes a scene)
video import [options]Create a video from your own scenes
video status <embedPostId>Check generation status + logs
video output <embedPostId>Retrieve the final output (MP4 URL, designId, editorUrl)

Video-only options (on top of the carousel options):

OptionDescription
--response-type <MP4|DESIGN>A video accepts nothing elsePDF/PNG are rejected. Default DESIGN
--video-duration <seconds>The whole video’s length, not per scene. At least 5, under 60. Required with --response-type MP4
--audio-id <id>Audio track ID from audio list — an ID, never a URL. Omit for a silent video
# AI-generated, rendered to MP4 postnitro video generate --context "3 habits that make remote teams faster" \ --response-type MP4 --video-duration 30 --wait # Your own scenes, with an audio track AUDIO=$(postnitro audio list | jq -r '.audios[0].id') postnitro video import --file ./scenes.json \ --response-type MP4 --video-duration 30 --audio-id "$AUDIO" --wait # Design only — no render; finish it in the video maker via editorUrl postnitro video generate --context "..." --wait

Rendering a video takes longer than a carousel — typically 15–45s with --wait and MP4, and longer for designs with animations or GIFs (which use the enhanced renderer). A saved PDF/PNG default doesn’t apply to a video: it’s treated as DESIGN and noted in warnings rather than failing the call. The duration and audio are reused automatically when the video is scheduled as a reel.

audio

List and delete the workspace’s audio tracks — the source of the --audio-id used by video, and of postSettings.audioId when scheduling a reel. See the Audio API.

CommandDescription
audio list [--page <n>] [--limit <n>]List audio tracks with their IDs
audio delete <id> --yesDelete a track and its stored file. Destructive, requires --yes
postnitro audio list postnitro audio delete aud_123 --yes

audio list returns { id, name, url, duration, artistName, source, createdAt } per track. Pass id as --audio-id; url is for previewing only. A track’s duration is independent of --video-duration — a longer track is cut off at the video’s length.

Uploading is done in the PostNitro app. The CLI (like the API) lists and deletes audio but cannot upload it. An empty list comes back with a note saying so — create the video without --audio-id in the meantime.

audio delete is refused while a scheduled post still references the track. Videos already rendered to MP4 keep their audio, since the track is baked into the file.

schedule

Manage scheduled posts and drafts. Full CRUD is also available via the Schedule API.

CommandDescription
schedule list --from <date> --to <date> [--accounts <id,id>]List scheduled posts/drafts in a date range, optionally limited to specific accounts
schedule get <id>Fetch a single scheduled post
schedule create [options]Create a scheduled post or draft
schedule update <id> [options]Update a post — REPLACES state, send the full intended body
schedule delete <id> --yesDelete a scheduled post/draft. Destructive, requires --yes

schedule create / update options:

OptionDescription
--status <DRAFT|SCHEDULED>Required. SCHEDULED publishes at the given time; DRAFT saves without publishing
--scheduled-at <iso>Required. ISO-8601 datetime, must be ≥ 5 minutes in the future (trailing Z)
--design-id <id>The design to attach (from output’s designIdnot the embedPostId)
--post-content <json>Captions keyed by platform: common, linkedin, instagram, tiktok, facebook, threads
--selected-accounts <json>Array of social-account IDs
--linkedin-post-settings / --instagram-post-settings / --tiktok-post-settings / --threads-post-settings <json>Per-platform settings — see Platform settings
--post-settings <json>Reel video settings, e.g. {"videoDuration":30,"audioId":"aud_123"}. Optional — see below
--file <path>JSON file with any of the above (inline flags override it)

A post must have either --design-id or non-empty --post-content. Hashtags are auto-extracted from captions.

--post-settings may be omitted for a reel: the API fills the duration and audio from the settings the attached design was generated with, then falls back to 30 seconds with no audio. videoDuration must be at least 5 and under 60, and audioId must be an audio ID in your workspace.

schedule list accepts --accounts <id,id> (comma-separated IDs from social list) to narrow the range to specific accounts. It filters posts, not the accounts within them: a post targeting LinkedIn and Instagram is returned when you filter by either, and it still reports both. Unknown IDs match nothing rather than erroring.

postnitro schedule create \ --status SCHEDULED \ --scheduled-at "2026-12-31T13:00:00Z" \ --design-id dsg_abc \ --selected-accounts '["acct_123"]' \ --linkedin-post-settings '{"postType":"document","postTitle":"5 remote work tips"}' \ --post-content '{"common":"New carousel 🚀 #remotework"}' postnitro schedule list --from "2026-01-01" --to "2026-12-31" postnitro schedule list --from "2026-01-01" --to "2026-12-31" --accounts "acct_123,acct_456" postnitro schedule delete sch_123 --yes

generate-and-schedule

One-shot: generate with AI → wait → schedule in a single call. May take 30–180s.

Takes every generate flag (--context required, --type, --instructions, --preset-id, …), plus --post-type CAROUSEL|IMAGE|VIDEO (default CAROUSEL; a video also takes --video-duration/--audio-id), the AI-image flags, and all the schedule create flags (--status and --scheduled-at required). --file here is the schedule body.

If generation succeeds but scheduling fails, the error returns the designId so you can retry schedule create without regenerating (and re-spending credits).

postnitro generate-and-schedule \ --context "How AI agents automate content" --type text \ --status SCHEDULED --scheduled-at "2026-12-31T12:00:00Z" \ --selected-accounts '["acct_123"]' \ --linkedin-post-settings '{"postType":"document","postTitle":"How AI agents automate content"}' \ --post-content '{"common":"New drop 🚀 #ai #automation"}'

import-and-schedule

One-shot: import your own content → wait → schedule in a single call. The import-side counterpart of generate-and-schedule.

OptionDescription
--post-type <CAROUSEL|IMAGE|VIDEO>Post kind (default CAROUSEL)
--slides <json>CAROUSEL/VIDEO slides as inline JSON (array or {"slides":[...]})
--slide <json>IMAGE slide as inline JSON (single object)
--slides-file <path>Slides from a file (array for CAROUSEL/VIDEO, object for IMAGE)
--video-duration <seconds> / --audio-id <id>VIDEO only — see video
--template-id / --brand-id / --response-type / --requestor-idAs in import
--status / --scheduled-atRequired (as in schedule create)
--design-id · --file · --post-content · --selected-accounts · --*-post-settings · --post-settingsScheduling body (as in schedule create)
AI-image flags--generate-images, --image-context, --image-placement, --image-strategy

Flag note: here --file is the schedule body (like generate-and-schedule), so slide content comes from --slides / --slide / --slides-file, not --file.

postnitro import-and-schedule \ --slides '{"slides":[ ... ]}' \ --status SCHEDULED --scheduled-at "2026-12-31T12:00:00Z" \ --selected-accounts '["acct_123"]' \ --post-content '{"common":"..."}'

AI image generation

Opt-in: have AI generate images and bake them into the design before rendering. Available on all six create commands and both one-shot commands — carousels, images, and videos alike.

FlagValuesDefaultNotes
--generate-images(boolean)offOpt in. Also implied by any flag below.
--image-context <text>stringRequired when generating images: a short visual brief for the prompts
--image-placement <mode>auto | background | in-lineautoauto = AI decides per slide
--image-strategy <mode>strategic | allstrategicstrategic ≈ 50% of slides; all = every eligible slide
postnitro carousel generate --context "How scheduling saves marketers time" \ --generate-images --image-context "upbeat and professional, product-focused" \ --image-placement auto --image-strategy all --wait
  • Best-effort. The post still completes if image generation fails or isn’t permitted. With --wait, the result carries an imageGeneration field (the GENERATE_IMAGES step); a FAILED status there explains why (e.g. free plan can’t generate AI images, or the org is over its AI-image quota). Without --wait, the same step appears in status logs.
  • Credits. AI images consume the organization’s separate AI-image quota — the post’s slide-based credits are unchanged.
  • Adds latency (real images are rendered).

Infographic layout

On either import command, set layoutType: "infographic" on a slide (with a layoutConfig) to render data columns (max 3) instead of an image. Works on both carousel slides and the single image slide.

layoutConfig fields (omitted fields use the defaults shown):

FieldTypeDefaultNotes
hasHeaderbooleantrueShow a header
columnCountnumber11–3
displayCounterAsstring"counter""counter" or "none"
columnDisplaystring"grid""grid" (comparative) or "cycle" (sequential — put all data in the first column)
columnDataarray[]The columns

Each columnData entry: id (string), header (string), content (array). Each content item: id (string), icon (string | null), title (string), description (HTML string, e.g. <p dir="ltr">…</p>), titleEnabled (boolean), descriptionEnabled (boolean).

Important: the id fields on columns and items are caller-provided and required — the API stores them as-is and does not auto-generate them. If layoutType is infographic but layoutConfig is missing, the slide falls back to the default layout.

{ "heading": "Our 2026 Results", "layoutType": "infographic", "layoutConfig": { "hasHeader": true, "columnCount": 3, "columnDisplay": "grid", "displayCounterAs": "counter", "columnData": [ { "id": "col-1", "header": "Growth", "content": [ { "id": "item-1", "icon": null, "title": "+42%", "description": "<p dir=\"ltr\">Revenue YoY</p>", "titleEnabled": true, "descriptionEnabled": true } ]} ] } }

Platform settings

Pass per-platform behavior via the --*-post-settings flags when scheduling. Settings are conditionally required based on the platforms among your --selected-accounts. See Platform settings for the full reference.

FlagShape
--linkedin-post-settings{"postType":"carousel|document|image|reel","postTitle":"..."}document needs a 5–90 char postTitle
--instagram-post-settings{"postType":"carousel|image|reel","postAsStory":false}
--tiktok-post-settings{"postType":"carousel|reel","privacyLevel":"PUBLIC_TO_EVERYONE|MUTUAL_FOLLOW_FRIENDS|SELF_ONLY","canComment":true,"canDuet":true,"canStitch":true,"autoAddMusic":false,"postTitle":null,"isBrandedContent":false,"isYourBrand":false,"isThirdPartyBrand":false,"isAIGeneratedContent":true}
--threads-post-settings{"postType":"carousel|image|reel"}
--post-settings (reel){"videoDuration":30,"audioId":"..."}

LinkedIn tip: PDF carousels are normally posted as postType: "document" (which needs a postTitle).

Response types & output

--response-type accepts four values (CLI default is PDF, except on video, which defaults to DESIGN):

ValueBehavior
PDFRenders and returns a PDF (single file URL in data)
PNGRenders and returns PNG image(s) (one URL per slide in data)
MP4Renders and returns a video (single file URL in data). video only
DESIGNSkips rendering. Creates the editable design only — no file. Fastest/lightest when you only need to schedule or edit

MP4 and the other rendered formats don’t mix: a video accepts only MP4 or DESIGN, and MP4 is rejected for carousels and images. MP4 also can’t be stored with defaults set, since it would break every carousel and image call — pass it per call.

Every completed output includes designId, name, size, and editorUrl. PDF/PNG/MP4 additionally include type, mimeType, and data (the file URLs); DESIGN omits those three.

// PNG output (excerpt) { "designId": "dsg_abc", "name": "5 remote work tips", "editorUrl": "https://postnitro.ai/app/carousel-maker/pn-xyz/dsg_abc", "type": "png", "mimeType": "image/png", "data": ["https://.../slide_0.png", "https://.../slide_1.png"] }

Environment variables

VariableRequiredDefaultDescription
POSTNITRO_API_KEYNo*Your PostNitro Embed API key
POSTNITRO_CONFIG_DIRNo~/.postnitro-cliWhere the saved key + defaults are stored

* Either --api-key, POSTNITRO_API_KEY, or a saved key (auth set-key) is required.

Errors & exit codes

Every command prints JSON. Failures go to stderr and exit non-zero:

{ "success": false, "error": { "message": "PostNitro API error (422): ...", "statusCode": 422 } }
  • Exit 0 — success (JSON on stdout)
  • Exit 1 — error (JSON on stderr)
Common errorFix
No API key foundPass --api-key, set POSTNITRO_API_KEY, or run auth set-key
Missing --brand-id ... multiple candidatesPass the ID or save one via defaults set
Scheduled at is in the pastUse a future ISO-8601 datetime (≥ 5 min ahead, trailing Z)
Slides must be an array with at least 3 entriesProvide starting + body + ending slides
An IMAGE post takes a single slide object, not an arrayUse --slide (object) for images, --slides (array) for carousels
AI image generation requires --image-contextAdd --image-context "<brief>" when using --generate-images
Refusing to delete ... without --yesAdd --yes to confirm destructive actions

Credits & pricing

The CLI consumes the same credits as the Embed API:

  • Content import: ~1 credit per slide
  • AI generation: ~2 credits per slide
  • AI images: consume the organization’s separate AI-image quota (not post credits); unavailable on the free plan
  • Response type (PDF/PNG/DESIGN) does not change credit cost

Command cheat sheet

# Auth postnitro auth set-key <key> | status | clear # Discovery + defaults postnitro template list | brand list | preset list | social list postnitro defaults set --template-id <id> --brand-id <id> --preset-id <id> --response-type PDF # Create (carousel, image, or video; async — use --wait; result has designId + editorUrl) # --response-type PDF|PNG|DESIGN (MP4 for video) | optional AI images: --generate-images --image-context "brief" [--image-placement …] [--image-strategy …] postnitro carousel generate --context "topic|url" --type text|article|x [--instructions "…"] --wait postnitro carousel import (--slides '{"slides":[…]}' | --file ./slides.json) --wait postnitro image generate --context "topic|url" --type text|article|x --wait postnitro image import (--slide '{"heading":"…"}' | --file ./slide.json) --wait postnitro video generate --context "topic|url" --response-type MP4 --video-duration 30 [--audio-id <id>] --wait postnitro video import (--slides '{"slides":[…]}' | --file ./scenes.json) --response-type MP4 --video-duration 30 --wait postnitro carousel status <embedPostId> | carousel output <embedPostId> # (also: image | video status | output) # Audio for videos & reels (upload happens in the app) postnitro audio list | audio delete <audioId> --yes # Brand kits postnitro brand create (--name --handle --image [--company-detail] | --data '{…}') postnitro brand update <id> # Schedule (use designId, not embedPostId; --scheduled-at ≥ 5 min in the future) postnitro schedule create --status SCHEDULED|DRAFT --scheduled-at "<iso>" --design-id <id> \ --selected-accounts '["<id>"]' --linkedin-post-settings '{"postType":"document","postTitle":"…"}' \ --post-content '{"common":"caption"}' postnitro schedule list --from "<date>" --to "<date>" [--accounts "<id>,<id>"] | get <id> | update <id> | delete <id> --yes # Social accounts postnitro social list | get <id> | disconnect <id> --yes # One-shot: create + schedule (both take --post-type CAROUSEL|IMAGE|VIDEO + the AI-image flags) postnitro generate-and-schedule --context "topic" --status SCHEDULED --scheduled-at "<iso>" postnitro import-and-schedule --slides '{"slides":[…]}' --status SCHEDULED --scheduled-at "<iso>" # or --slide for image # Help postnitro --help | postnitro <command> --help | postnitro <command> <subcommand> --help

CLI vs. MCP vs. Embed API vs. Embed SDK

Best for
CLITerminal workflows, shell scripts, CI pipelines, and AI agents that shell out — JSON in, JSON out
MCP ServerAI assistants and agents creating and scheduling posts conversationally (Claude, Cursor, custom agents)
Embed APIProgrammatic automation — Make.com, Zapier, n8n, cron jobs, custom backends
Embed SDKLetting your users visually create and edit carousels inside your web app

Support

Last updated on