Command-line tool (CLI)

Schedule, publish and manage posts from your terminal, your scripts and your CI.

The bulkit command does everything the API does, from a terminal. Use it to send a quick post, to script your publishing, or to post from a CI job when you ship a release.

bulkit posts create -c @acme -c linkedin -t "Shipping today." --at +2h
  • Asks before anything public or destructive, unless you pass --yes
  • --json output and stable exit codes for scripts
  • Tab completion for bash, zsh and fish

Install

npm install -g bulkit-cli

Needs Node.js 22.12 or later. Check it worked with bulkit --version.

To try it without installing, run npx bulkit-cli --help.

Log in

Create an API key, then:

bulkit auth login

Paste the key when asked. What you type is hidden. Check which key is in use at any time with bulkit auth status.

Start with a test key (bk_test_…). It checks every request like a live key, but never posts and is never billed. With a test key the CLI also doesn't ask before publishing, since nothing can go out.

The key is saved in ~/.config/bulkit/config.json (%APPDATA%\bulkit on Windows), and only your user can read it.

Several workspaces

Save one key per workspace under a profile, and pick it with --profile:

bulkit auth login --profile acme
bulkit posts list --profile acme

You can also set BULKIT_PROFILE=acme instead of passing --profile every time.

Your first post

# 1. What can I post to, and what does each platform accept?
bulkit channels list
bulkit platforms get x

# 2. Save a draft to two channels.
bulkit posts create -c x -c linkedin -t "Hello from the Bulkit CLI." --draft

# 3. Look at it.
bulkit posts list

posts create makes one post for every --channel you give, with one id. Don't run the command once per channel: that makes separate posts, each with its own schedule.

Pick channels by name

--channel (-c) takes any of these. They're tried in this order:

You giveExampleMatches
The channel id-c cmtu8u6n4000…That channel.
The username-c @acmeThe channel with that username.
The display name-c "Acme Inc"The channel with that name.
The platform-c linkedinYour one LinkedIn channel.

If a value matches more than one channel (for example -c instagram when you have two Instagram accounts), the CLI stops and lists them instead of guessing. Pass the id then.

Draft, schedule or publish now

FlagWhat happens
--draftSaved as a draft. This is the default.
--at <time>Scheduled. Give a time like 2026-11-01T09:00 (your time zone), 2026-11-01T09:00Z, or an offset from now: +30m, +2h, +1d, +1w.
--nowPublished right away, to real accounts.
bulkit posts create -c x -t "Launch day." --at +2h
bulkit posts create -c x -t "We're live!" --now

With a test key, --at and --now are checked and the post is saved as a draft. The CLI says so: "Test mode: nothing was published or scheduled."

Threads, media and titles

# A thread: one --text per part.
bulkit posts create -c x -t "1/ Big news" -t "2/ Here is why" --draft

# Long text from a file (or - for stdin).
bulkit posts create -c linkedin --text-file announcement.md --draft

# A YouTube video with a title.
id=$(bulkit media upload launch.mp4)
bulkit posts create -c youtube -m "$id" --title "Launch" -t "Our new release." --draft

Different text per channel

Every channel gets the same text with --text. To write a different text for each channel, or to set platform options, write the request body as JSON and pass it with --file. The body is the same as POST /posts:

post.json
{
  "channels": [
    { "channelId": "chn_x", "blocks": [{ "content": "Shipping today." }] },
    { "channelId": "chn_li", "blocks": [{ "content": "A longer version, for LinkedIn." }] }
  ]
}
bulkit posts create --file post.json --at +1d

--file - reads the body from stdin. Add --dry-run to print the body without sending it.

Upload media

bulkit media upload photo.jpg clip.mp4
bulkit media upload https://cdn.example.com/launch.png

The command prints one media id per file, ready for --media (-m). Large videos are uploaded in parts automatically.

A URL is downloaded by the CLI, then uploaded like a local file. So it only has to work from your computer, not from Bulkit's servers. Put quotes around URLs that contain & or ?, or your shell will cut them short:

bulkit media upload 'https://cdn.example.com/photo.jpg?width=1200&quality=85'

See Media for the sizes and types each platform accepts.

Publishing is asynchronous

--now and bulkit posts publish return as soon as the post is queued. That doesn't mean it's live yet. Add --wait to wait until every channel has finished:

bulkit posts create -c x -c linkedin -t "We're live!" --now --wait

With --wait, the command exits with code 1 if any channel failed, and prints why. You can also check later:

bulkit posts get <post-id>          # where each channel stands
bulkit posts get <post-id> --wait   # wait for it to finish
bulkit posts publish <post-id>      # retry; channels that already published are skipped

See Check the result for what each status means.

Safety

Everything the CLI does with a live key happens on real accounts. So it asks before:

  • publishing (--now, posts publish), with a live key
  • replying to a comment, with a live key (it shows you the exact reply first)
  • replacing, revising or deleting a post, deleting media, and deleting a webhook or changing its secret, with any key

bulkit posts retract deletes the published posts from the platforms, with their likes and replies. It can't be undone, so with a live key the CLI asks you to type the post id.

Pass --yes (-y) to skip the question. When there's no terminal to ask in (a script, a CI job, --no-input), the CLI never waits for an answer: it stops with exit code 2 and tells you to add --yes.

posts retract and posts delete are different. Retract takes the post down from the platforms. Delete only removes it from Bulkit and leaves it live. See Manage posts.

Two more ways to stay safe:

  • posts create --dry-run prints the request without sending it.
  • posts create --idempotency-key <key> makes a retry safe: run the same command with the same key again and you won't get a second post.

Use it in scripts and CI

Output

  • --json prints the API's own JSON on stdout. Use it in scripts: the normal output may change between versions, the JSON won't.
  • When the output goes to a pipe or a file, tables print as tab-separated rows without a header, so cut and awk work.
  • posts create and media upload print only the id on stdout. All messages go to stderr. So this works:
id=$(bulkit posts create -c x -t "Hello" --draft)
bulkit posts list --status failed --json | jq -r '.[].id'

With --json, errors are JSON too, on stderr:

{ "error": { "message": "Post pst_123 not found", "code": "not_found", "status": 404, "requestId": "req_…", "exitCode": 4 } }

Exit codes

CodeMeaning
0Success.
1Something failed: an API error, a channel that failed to publish (with --wait), a cancelled question.
2The command was wrong: unknown command or flag, bad value, or --yes missing where there's no terminal.
3Not logged in, or the key was refused or lacks the permission.
4Not found.
5Your plan's limit for this period is used up. See bulkit usage.
130Stopped with Ctrl-C.

In CI

Store the key as a secret and pass it in BULKIT_API_KEY. It's used instead of any saved key, and nothing is written to disk:

.github/workflows/announce.yml
- name: Announce the release
  run: npx bulkit-cli posts create -c x -c linkedin -t "v${{ github.ref_name }} is out." --now --yes
  env:
    BULKIT_API_KEY: ${{ secrets.BULKIT_API_KEY }}

To log in from a script instead, pipe the key in. Never put a key in a command-line flag, where other users and your shell history can see it:

bulkit auth login --with-key < key.txt

Shell completion

Tab completes commands, flags, and values like --status. In zsh and fish you also see what each one does. Add one line to your shell's startup file, then open a new terminal:

echo 'eval "$(bulkit completion zsh)"' >> ~/.zshrc

Put it after the line that runs compinit, if you have one.

echo 'eval "$(bulkit completion bash)"' >> ~/.bashrc
bulkit completion fish > ~/.config/fish/completions/bulkit.fish

Environment variables

VariableWhat it does
BULKIT_API_KEYThe API key to use, instead of the saved one.
BULKIT_PROFILEThe profile to use when --profile isn't given.
BULKIT_BASE_URLSend requests to another API address (a proxy, a local server).
BULKIT_CONFIG_DIRWhere to keep config.json.
BULKIT_DEBUG=1Same as --debug.
BULKIT_NO_INPUT=1, CISame as --no-input: never ask.
BULKIT_NO_UPDATE_NOTIFIER=1Don't check for a newer version. It's checked at most once a day, and never in CI.
NO_COLOR, FORCE_COLORTurn colors off or on.

Troubleshooting

Something failed and you don't know why. Run the command again with --debug. It prints every request, its status and its request id. It never prints your key.

You need help from us. Every API error shows a request id (req_…). Send it with your question: it lets us find exactly what happened.

A URL upload stopped at &. Put the URL in single quotes. See Upload media.

Next steps

On this page