For AI agents: a documentation index is available at /llms.txt. Every guide and example is also available as markdown by adding .md to its URL.
CLI
@schedulin/cli is the official command-line interface for the Schedulin API. It wraps the
same /v0 REST endpoints as the SDKs, so anything you can script with an API key you can do
from a shell — list channels, draft and publish posts, upload media, pull analytics.
Install
Run it on demand with npx (no install):
npx @schedulin/cli accounts list
Or install it globally for a shorter schedulin command:
npm i -g @schedulin/cli schedulin --help
Requires Node.js 18+.
Authentication
The CLI needs a workspace API key (see Authentication). It resolves the key in this order — highest wins:
--api-key <key>flagSCHEDULIN_API_KEYenvironment variable- Stored config file
Store it once and forget it:
schedulin config set api-key <your-api-key> schedulin whoami # verify the key works
The key is written to ~/.config/schedulin/config.json (or $XDG_CONFIG_HOME/schedulin/) with
0600 permissions and is masked in all output. Run schedulin config show to see the resolved
config (key masked).
Global flags
| Flag | Description |
|---|---|
--api-key <key> | API key (else SCHEDULIN_API_KEY or stored config) |
--base-url <url> | API base URL (else SCHEDULIN_API_BASE or production) |
--json | Output raw JSON instead of a table — for piping into jq. Errors are printed as JSON on stderr |
--dry-run | For writes: print the request that would be sent, then stop |
-y, --yes | Skip confirmation prompts (required to publish/delete non-interactively) |
-v, --verbose | Log requests to stderr (API key masked; presigned upload URLs logged without their signature) |
Common commands
# Channels (social accounts) schedulin accounts list schedulin accounts next-slots <accountId> # upcoming queue slots schedulin accounts timezone <accountId> America/New_York # IANA zone names only schedulin accounts tiktok-info <accountId> # TikTok privacy options, limits schedulin accounts pinterest-boards <accountId> # board ids for platformConfiguration.board_ids schedulin accounts discord-channels <accountId> # channel ids for --channel schedulin accounts slack-channels <accountId> schedulin accounts delete <accountId> # disconnect: posts kept, can reconnect schedulin accounts delete <accountId> --permanent # deletes the account AND ALL its posts # Posting requirements per platform (caption limits, media rules, # platformConfiguration JSON Schema) schedulin platforms schedulin platforms tiktok # Posts — one post targets one social account schedulin posts list --status SCHEDULED --limit 20 schedulin posts list --statuses COMPLETED,FAILED --social-account-id <id> schedulin posts get <postId> schedulin posts create \ --social-account-id <id> \ --caption "Hello from the CLI 👋" \ --media https://www.schedulin.app/opengraph-image.png \ --action schedule \ --scheduled-at 2030-01-15T15:00:00Z schedulin posts create --social-account-id <id> --thread "1/ first" "2/ second" # X / Mastodon schedulin posts create --social-account-id <discordId> --caption "Release notes" --channel <channelId> schedulin posts update <postId> --caption "Edited" --scheduled-at 2030-01-16T15:00:00Z schedulin posts update <postId> --media https://www.schedulin.app/opengraph-image.png # replaces media schedulin posts publish <postId> # publish a draft now schedulin posts publish <postId> --scheduled-at 2030-01-15T15:00:00Z schedulin posts tags <postId> --tag <tagId> schedulin posts delete <postId> # a published post stays live on the platform schedulin posts analytics summary <postId> schedulin posts analytics series <postId> # Tags schedulin tags list --search launch schedulin tags create --name launch --color "#a855f7" # Media schedulin media upload ./image.jpg # prints the media id + url to pass to --media schedulin media from-url https://www.schedulin.app/opengraph-image.png schedulin media upload-link # page link for uploading from a device schedulin media register <key> # add a presigned upload to the library, in place schedulin media list --type image --page 2 schedulin media get <mediaId> schedulin media delete <mediaId> # Webhooks schedulin webhooks create --url https://example.com/hook --events post.published,post.failed schedulin webhooks list schedulin webhooks get <webhookId> schedulin webhooks update <webhookId> --enabled false schedulin webhooks test <webhookId> schedulin webhooks deliveries <webhookId> schedulin webhooks rotate-secret <webhookId> schedulin webhooks delete <webhookId>
Post statuses are SCHEDULED, PROCESSING, COMPLETED, DRAFT, and FAILED (the CLI accepts
any case). schedulin posts create defaults to --action draft, so nothing publishes unless you
ask it to. To schedule or publish an existing draft, use posts publish (optionally with
--scheduled-at) or posts update --status SCHEDULED. Run schedulin <command> --help for the
full flag list of any command.
Dates and scheduling
--scheduled-at (and --from, --to, --after) take ISO-8601 date-times such as
2030-01-15T15:00:00Z. The CLI checks them before sending anything:
- A value that isn't a date (
--scheduled-at tomorrow), or a date that doesn't exist (2030-02-30), is rejected locally. --action schedulewithout--scheduled-atis rejected locally, before the confirmation prompt. Use--action queueto take the account's next queue slot instead.- A time in the past is rejected — scheduled posts need a future time, and so does any
posts update --scheduled-at. --limitand--pageare checked against the API's bounds (for exampleposts list --limitmust be 1–200).
Platform-specific flags
--threadpublishes a reply chain on X (Twitter) and Mastodon only. On any other platform only the first segment is used, as the caption. On X the segments are the whole thread (--captionisn't posted, and--mediagoes on the first tweet); on Mastodon a--captionbecomes the root toot and every segment is a reply.--titlesets the top-leveltitle, which Facebook (video title), YouTube (fallback title), and LinkedIn (document title) use. Pinterest, Reddit, YouTube, dev.to, Hashnode, WordPress and others readplatformConfiguration.titleinstead — pass it with--platform-config '{"title":"…"}'.--channel <id>setsplatformConfiguration.channel, which Discord and Slack posts require. List an account's channels withschedulin accounts discord-channels <id>orschedulin accounts slack-channels <id>.
When you use --thread or --title, the CLI looks up the account's platform and warns if that
platform will ignore the flag.
Attaching local files
--media takes URLs. A public URL works as-is; for a local file, upload it first and pass the
returned url:
URL=$(schedulin --json media upload ./launch.jpg | jq -r .url) schedulin posts create --social-account-id <id> --caption "Launch day" --media "$URL"
media upload sends files up to 4 MB as one multipart request (POST /v0/media/upload). Larger
files (or --via presign) are uploaded straight to storage with a presigned PUT, then
registered in your media library in place with POST /v0/media/register — no second copy is
stored. Either way you get back a media id and url, the file shows up in media list, and
media delete removes it from storage. If the register step fails, the CLI warns and prints
the stored url, which still works in --media.
Pass --no-library to skip the register step; the printed URL still works in --media, and
schedulin media register <key> adds it to the library later. Against an older API that has
no register endpoint (404), the CLI falls back to POST /v0/media/from-url, which imports a
copy and leaves the presigned original in storage — it warns when that happens.
The low-level schedulin media presign command only returns a presigned URL and creates no
media-library entry; after your own PUT, run schedulin media register <key>.
Safety
The CLI is built so a script can never publish or delete by accident:
--dry-runprints the resolved request body and exits before any write, including deletes. The client also refuses to send any non-GET request in dry-run mode, so nothing is ever changed.- Publishing actions (
--action now/queue/schedule,posts update --status SCHEDULED|PROCESSING,posts publish), deletes, andwebhooks rotate-secretprompt for confirmation. The prompt defaults to No: pressing Enter aborts. - In a non-interactive context (no TTY, or
--json) they abort unless you pass--yes. accounts deleteonly disconnects the account (posts are kept).accounts delete --permanentdeletes the account and all of its posts, and--yesalone never skips that confirmation: scripts must pass--yes --i-understand, and in a terminal you type the account id.
Exit codes
| Code | Meaning |
|---|---|
0 | Success. Also used when the reading end of a pipe closes early, for example when piping into head |
1 | Any error: API error, network error, invalid flag, or a declined/refused confirmation |
130 | A confirmation prompt was cancelled with Ctrl-C |
Errors and rate limits
Failed requests print the HTTP status, error code, and a human-readable message. Validation
errors (422) list every invalid field. If the API can't be reached, the error names the host
and the reason, for example Could not reach api.schedulin.app (getaddrinfo ENOTFOUND …).
With --json, errors are printed to stderr as JSON so scripts can parse them:
{"error":{"status":422,"code":"INPUT_VALIDATION_FAILED","message":"Input validation failed","fieldErrors":{"scheduledAt":["…"]}}}
Network failures use "code":"NETWORK_ERROR", invalid flags (--limit abc) use
"code":"USAGE_ERROR", and a confirmation refused in non-interactive mode uses
"code":"CONFIRMATION_REQUIRED". Deletes print {"deleted":true,"id":"…"} on stdout.
Colors are only used when writing to a terminal (NO_COLOR / FORCE_COLOR override), so
piped output never contains ANSI escape codes.
Rate-limited reads (429) are retried automatically after the Retry-After delay. Writes are
never replayed automatically; the CLI tells you how long to wait instead. See
Error handling and Rate limits.
Scripting
Use --json to get machine-readable output and pipe it into jq:
# IDs of every connected account schedulin accounts list --json | jq -r '.data[].id' # Number of scheduled posts schedulin posts list --status SCHEDULED --json | jq '.total'
List endpoints return their array under a data key ({ "data": [...] }); posts list
returns { posts, page, total, totalPages } and media list returns
{ items, page, total, totalPages }. See Pagination.