logo
Schedulin Developers

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:

  1. --api-key <key> flag
  2. SCHEDULIN_API_KEY environment variable
  3. 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

FlagDescription
--api-key <key>API key (else SCHEDULIN_API_KEY or stored config)
--base-url <url>API base URL (else SCHEDULIN_API_BASE or production)
--jsonOutput raw JSON instead of a table — for piping into jq. Errors are printed as JSON on stderr
--dry-runFor writes: print the request that would be sent, then stop
-y, --yesSkip confirmation prompts (required to publish/delete non-interactively)
-v, --verboseLog 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 schedule without --scheduled-at is rejected locally, before the confirmation prompt. Use --action queue to 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.
  • --limit and --page are checked against the API's bounds (for example posts list --limit must be 1–200).

Platform-specific flags

  • --thread publishes 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 (--caption isn't posted, and --media goes on the first tweet); on Mastodon a --caption becomes the root toot and every segment is a reply.
  • --title sets the top-level title, which Facebook (video title), YouTube (fallback title), and LinkedIn (document title) use. Pinterest, Reddit, YouTube, dev.to, Hashnode, WordPress and others read platformConfiguration.title instead — pass it with --platform-config '{"title":"…"}'.
  • --channel <id> sets platformConfiguration.channel, which Discord and Slack posts require. List an account's channels with schedulin accounts discord-channels <id> or schedulin 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-run prints 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, and webhooks rotate-secret prompt 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 delete only disconnects the account (posts are kept). accounts delete --permanent deletes the account and all of its posts, and --yes alone never skips that confirmation: scripts must pass --yes --i-understand, and in a terminal you type the account id.

Exit codes

CodeMeaning
0Success. Also used when the reading end of a pipe closes early, for example when piping into head
1Any error: API error, network error, invalid flag, or a declined/refused confirmation
130A 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.