CLI (plugsync-cli)
The plugsync command-line client manages connectors and plugins like code: pull a connector’s configuration to a local directory, edit it, diff it against a published version, validate it, preview a publish, push it back to the draft, and author plugins — all against the plugsync API, with no checkout of the plugsync repository required.
It ships as a standalone package on PyPI (plugsync-cli), on its own release cadence, independent of the platform’s.
Install
pip install plugsync-cliRequires Python 3.12+. Verify with plugsync --version. Install a specific release with pip install plugsync-cli==X.Y.Z.
Configure and authenticate
plugsync auth login -e you@example.com-
api_urldefaults tohttps://app.plugsync.com(the hosted platform) — nothing to configure for the common case. Point it at a different deployment withplugsync config set api_url <url>. -
plugsync auth loginwith email/password (--email/-e,--password/-p; whichever you leave out is prompted for) saves the returned JWT asapi_keyin~/.plugsync/config.yaml; every subsequent command reuses it as aBearertoken. That JWT expires in 15 minutes — fine for an interactive session, wrong for scripts. -
For scripts and CI, use a long-lived org API key (
ps_org_..., created from Settings → API Keys in the dashboard, owner-only) instead:plugsync auth login --token ps_org_... # or -t ps_org_... -
PLUGSYNC_API_URL/PLUGSYNC_API_KEYenvironment variables override the config file on every command — the cleanest option for CI, no config file involved.plugsync config get api_url(orapi_key) prints the value a command would actually use, environment variable or hosted default included, and printsapi_keyin full;plugsync config showlists only what is in the config file, withapi_keymasked to its first characters:export PLUGSYNC_API_KEY=ps_org_... plugsync pull my-connector
Keeping the stored credential safe
~/.plugsync/config.yaml stores the credential in plain text, with your
system’s default file permissions. Treat it like a password file:
- restrict it to your own user (
chmod 600 ~/.plugsync/config.yaml) and keep it out of shared folders, backups and repositories; - in CI, prefer the
PLUGSYNC_API_KEYenvironment variable, fed from your CI system’s secret store, over a config file on the runner; plugsync auth logoutonly deletes the local copy: it does not revoke the key. If an org API key may have been exposed, revoke it from Settings → API Keys in the dashboard.
Org keys authenticate the config plane only: they do not work against the Event Ingestion API (/api/v1/events), which requires a per-connector ps_live_... key. See Getting started for the full key-type breakdown.
Connector-as-code workflow
plugsync pull <connector> downloads a connector’s draft config into a local directory. The directory is created in the current directory and named after the connector; --output <path> (-o) writes it somewhere else, under whatever name you choose. The directory is the config v3 blob: plugsync.yaml carries every top-level key, and the named collections become one file per item.
my-connector/
├── plugsync.yaml # version, settings, conflicts, pipelines
├── entities/
│ ├── contacts.yaml # one file per entity
│ └── orders.yaml
├── flows/
│ └── shop-orders.yaml # one file per event flow
└── sources/
└── hubspot.yaml # one file per event source.plugsync.yaml is written alongside it, recording which connector the directory tracks and which draft version it was pulled from. Keep it: push uses it.
The config is a JSON document, and the CLI reads these files as such: a date written unquoted (cutoff: 2026-07-26) stays the string you typed rather than becoming a YAML timestamp JSON has no type for. Anything the config cannot carry — !!binary, .inf, .nan — is refused by file and key instead of failing mid-upload, and the rare value the reader has to convert (an explicit !!timestamp, a !!set) is printed as it converts it. Quote anything you mean as text all the same: YAML 1.1 still reads yes as the boolean true and 1:30 as the number 90, and those are valid JSON values the CLI has no reason to second-guess.
Edit the files, then round-trip:
plugsync diff my-connector # local files vs. the newest published version
plugsync validate my-connector # preflight the draft, non-zero exit on an error
plugsync preview my-connector # what a publish would change, plus findings
plugsync push my-connector # upload to the connector's draftpush writes to the connector’s draft and stops — same write side as the dashboard and the AI agent (draft → publish workflow). Publishing is a separate, explicit act: plugsync push --publish -m "Added phone", which runs preflight first and refuses to publish while any error stands.
push also guards the write with the draft version recorded at pull time: if someone edited the connector in the dashboard meanwhile, the push is rejected rather than overwriting their work. Re-pull and reapply, or push --force to overwrite deliberately.
A publish also resolves values the draft does not carry — the plugin function ARN and version, HubSpot schema provisioning — and those can drift while the source stays identical (a plugin promoted to a new live version, for instance). push --publish on an unchanged draft answers “Nothing to publish”; push --publish --republish asks for a forced publish that refreshes them (see forcing a re-publish). It is distinct from --force, which is about overwriting a concurrent edit.
Preflight findings are printed with their severity: error, warning or info, each counted in a summary line. Only validate treats an error as a failure (non-zero exit, for pipelines); preview reports and always exits 0.
A finding about the HubSpot scopes of the connected portal says how to fix it. When the config needs scopes the portal’s token does not grant (object_type_missing_scopes, attachment_missing_scopes, submit_form_missing_forms_scope), the finding prints a scopes: line with the scopes to add and a reauthorize: line with the command that asks for them:
! warning object_type_missing_scopes: Entity vehicle syncs HubSpot object type 'p12345678_vehicle' ...
in: vehicle
scopes: crm.objects.custom.read, crm.objects.custom.write
reauthorize: plugsync connector reauthorize <connector-id> --scopes crm.objects.custom.read,crm.objects.custom.writeplugsync connector reauthorize <name> prints the HubSpot consent link for the portal, valid for 60 minutes: open it in a browser signed in to that portal, approve, and you land back on the connector in the dashboard (which offers the same re-authorization under Review changes). --scopes a,b adds scopes to the ones already granted; a scope the plugsync app does not declare is refused and named. Then run plugsync validate to check again.
Two findings say the scope check did not run, so a clean preflight is not read as “the scopes are fine”:
hubspot_scopes_not_checked(warning): the portal’s token could not be inspected — no usable HubSpot connection on the connector, or HubSpot did not answer. Check the connection and run the preflight again.object_type_scopes_not_checked(info): an entity syncs an object type that cannot be mapped to a scope, so nothing was checked for it. This is the case of a custom object written as its type id (2-12345) or as a bare name (vehicle): write it asp<portalId>_<name>to have its scopes checked.
These findings are advisory, like every warning: they never block a publish. A plain plugsync push (no --publish) prints the scope findings, and only those, right after saving the draft, so a missing scope is on screen before anyone publishes; the full preflight stays with validate, preview and push --publish.
diff names the keys that differ, with the nested path for the ones inside schemas/steps/config (schemas.hubspot.object_type, steps[0].target, config.triggers[0].pattern), so a modified entity, flow or source says where to look. Keys that exist only in the published version and hold that field’s schema default get their own rows, marked *_server_default, and are left out of the change count: the backend materializes every default the input omitted when it saves a draft (on_target_deleted: "skip", pipelines: [], settings: {}), so a hand-written directory that does not carry them has not been modified by anyone. The CLI reads the schema from GET /api/config-surface (see config-surface.md) at diff time; against a backend too old to serve it, diff still reports the difference and says why it cannot tell the two apart. Read-only throughout, and no diff result is a failure: whatever it finds, diff exits 0 (only being unable to run at all — no .plugsync.yaml, a directory that does not declare its sources, an unreadable version, an API error — exits non-zero).
History is available too: plugsync log <connector> lists published versions, plugsync pull <connector> --revision N pulls an old one (read-only history, not the draft), plugsync rollback <version> <connector> restores a previous version into the draft (again: restores, does not publish). One asymmetry to know about with the sources/ directory below: pull --revision N of a version published before sources were part of the config carries no sources key, so it removes the local sources/ directory, and the push that follows is refused (see below). pull --revision followed by push is therefore not equivalent to plugsync rollback where sources are concerned. Use rollback, which restores the whole config blob into the draft.
Naming the connector: name or id
Available from CLI 0.9.0.
Every command that takes a connector as an argument or an option accepts its name or its id: pull, log, rollback, the whole connector group and plugin logs --connector. Earlier releases accepted only the name on pull, log and rollback, and only the id on plugin logs --connector.
plugsync pull my-connector
plugsync pull 3f2b8c1e-5d47-4a90-b6e1-0c9d2a7f4e15 # same connector, same directory: ./my-connectorA pull by id still writes to the directory named after the connector, so both forms land in the same place. push, diff, validate and preview take a connector directory and read the connector’s id from its .plugsync.yaml, so they never depended on the name.
When the reference matches more than one connector (two connectors of the organization share a name), the command stops with a non-zero exit, lists the id and name of each match and asks for the id. Nothing is read or changed before that. Earlier releases silently picked the first match.
Event sources in the directory
Available from CLI 0.6.0.
sources/<name>.yaml holds one event source definition each: its type and its config, with the source’s name taken from the file name, exactly as entities and flows work. That is the reviewable half of a source — the HubSpot triggers deciding which property change emits an event, a poller’s URL and schedule, a webhook’s signature algorithm — so a pull brings it down as text, diff names the keys that changed inside it, and push sends it back with the rest of the directory. The definition travels with the connector config: push writes it to the draft, and what happens to it at publish is described in the event sources reference.
What is not in these files: the webhook secret, the enabled flag, and the runtime state a source accumulates (poller cursors, last poll, last error). Those stay on the source itself, managed from the dashboard and the /sources endpoints, and a push never touches them.
Every connector config declares its sources, and an empty list is a declaration like any other: sources: [] says the connector has no sources, and publishing it removes any source the connector had.
| In the directory | In the config | Meaning |
|---|---|---|
empty sources/ directory (only .gitkeep) | sources: [] | the connector has no sources |
sources/*.yaml | sources: [...] | the connector has these sources |
no sources/ directory, no sources key in plugsync.yaml | none | the directory does not declare its sources: refused |
A directory that does not declare its sources is refused before anything is sent: push, validate, preview and diff stop with a non-zero exit and name three ways out. Run plugsync pull to realign the directory with the draft; or create sources/ with an empty .gitkeep inside (plus one file per source, if the connector has any); or write sources: [] in plugsync.yaml if the connector really has no sources. The CLI never fills the gap on its own, because an empty list in place of a missing directory would delete every source of the connector at the next publish. Because an empty directory is not something a repository can carry on its own, the CLI writes a .gitkeep inside sources/ when it holds no source: commit it, or a fresh clone of the repository reads back as a directory that does not declare its sources.
A published version from before sources were part of the config has no sources key. diff against such a version shows a sources_key_added row, since your directory declares sources the published version does not carry: publishing makes the config the authority for the connector’s sources.
A push the API refuses names the file to open. The API describes a rejected config by the path the key has in the document (sources[hubspot].config.triggers[0].pattern); when the error carries the source’s name like that, the CLI prints the API’s explanation and, under it, sources/hubspot.yaml: config.triggers[0].pattern.
Upgrading from 0.5.0 or earlier. Those releases carry sources as an opaque top-level key in plugsync.yaml and send it back untouched, so nothing was lost by them: 0.6.0 reads that key and the next pull or push moves it into sources/ on its own. A directory that ends up with the key inline and a sources/ directory is refused instead of resolved by guesswork — a push replaces the whole draft, so picking one of the two lists would silently delete the other. Delete whichever of the two is stale. A directory pulled by one of those releases from a connector whose config had no sources key carries neither, and is refused as described above: a fresh plugsync pull fixes it.
Retrying a flow whose circuit breaker is open
When a flow keeps failing, its circuit breaker opens: that one flow is skipped while every sibling flow on the connector keeps syncing, and the flow retries itself on a schedule that starts at 5 minutes and climbs. The two operational moves that go with it are in the CLI, so a blocked flow can be inspected and retried from a script or a pipeline instead of the dashboard:
plugsync connector breakers my-connector # which flows are blocked, and why
plugsync connector flow retry my-connector shop-orders # retry one nowconnector breakers reads the connector’s health block (the same one GET /api/connectors/{id} returns) and prints a row per flow whose breaker is not closed: the flow name, its state (open or half_open), the reason recorded when it opened, how long ago that was, when the next automatic attempt is due, and how many times in a row the flow has tripped. It prints No open circuit breaker on '<name>' when there is nothing to show, and exits 0 either way — a blocked flow is a state to read, not a command failure.
connector flow retry moves that flow’s breaker to half_open, which makes the next message through the flow the recovery attempt instead of waiting out the timer. It is not a force-close: if the attempt succeeds the breaker closes, and if it fails the breaker re-opens with a longer wait, exactly as an automatic attempt would. Running it on a flow that is already mid-attempt changes nothing. Running it on a flow that is not blocked at all exits non-zero saying there is nothing to retry, which is the same answer the API gives (404).
Both commands take a connector name or id and need only the org API key, so they work unattended:
export PLUGSYNC_API_KEY=ps_org_...
plugsync connector flow retry 3f7c1e9a-... shop-ordersForce-closing a breaker outright, bypassing the recovery attempt, is a platform-side operation and is deliberately absent here: org API keys cannot perform it. What the connector’s own owner can do is the retry above, or PATCH /api/connectors/{id} with {"status": "active"} from the dashboard, which closes every flow’s breaker on the connector at once.
Polling a connector on demand
Available from CLI 0.9.0.
A poller, scheduled or file source runs on its own cadence. To run it now — after fixing a credential, while testing a new flow, or when a file has just been dropped and the schedule is hours away — queue a poll from the CLI:
plugsync connector poll my-connector # every polled source of the connector
plugsync connector poll my-connector --source erp-orders # only that oneThe poll covers the enabled poller, scheduled and file sources of the published config and ignores their schedule: it runs whether or not the cron is due. --source <name> (-s) narrows it to one source. The command returns as soon as the poll is accepted, not when it has finished: the events it finds show up in the connector’s events like those of a scheduled poll. The same poll asked again within 5 minutes of the previous one (same connector, same --source or none) is accepted but not run again, so wait 5 minutes before repeating it.
Sources that receive their events instead of fetching them (hubspot, webhook, event_api) are never part of a poll. Naming one with --source, or a source that is disabled, or a name the published config does not have, exits non-zero with the API’s reason and the list of sources that can be polled:
source 'hubspot' is a 'hubspot' source, which is never polled: only poller, scheduled, file sources can be polled on demand. (source_not_pollable)
Sources of 'my-connector' that can be polled: erp-orders, nightly-filesA source declared only in the draft is not there yet: publish first. A connector that is paused or stopped on an error is refused too. The command takes a connector name or id and works with the org API key. The endpoint behind it is described in Polling a source on demand.
Pre-warming the identity map before a cutover
Available from CLI 0.8.0.
A connector starts with an empty identity map. When it replaces a previous integration, the HubSpot portal already holds the records, so the first event of each one has to search for its record before writing it — all of them at once, at go-live. The pre-warm reads the portal ahead of time and writes the mappings that are missing, so those first events update their record directly:
plugsync connector prewarm start my-connector --dry-run --wait # count what would be mapped, write nothing
plugsync connector prewarm start my-connector --wait # the full pass, days before go-live
plugsync connector prewarm start my-connector --modified-after 2026-10-01T08:00:00Z --wait # top-up right before the switchThe job runs on the server, so closing the terminal does not stop it. It covers every entity of the published config that has a HubSpot schema and an identity.property (publish first: the draft is not read), and it only reads HubSpot. What it writes is restricted to mappings whose key is free:
- an existing mapping is never overwritten;
- a value held by more than one HubSpot record is reported as ambiguous and left unmapped;
- a record already mapped under another external id is reported as mapped elsewhere and left as it is.
| Command | What it does |
|---|---|
prewarm start <connector> | Start a job. --dry-run counts without writing; --modified-after <timestamp> (ISO 8601 with a timezone) limits the pass to records modified after that moment; --entity <name> (repeatable) limits it to some entities; --store-id <store> is only for records that will arrive through bulk import with that storeId |
prewarm status <connector> [job-id] | Show a job’s progress; without an id, the most recent job |
prewarm resume <connector> <job-id> | Continue a failed or interrupted job from its last saved page |
prewarm cancel <connector> <job-id> | Cancel a job for good |
prewarm findings <connector> <job-id> | List the records that were not mapped (--finding ambiguous|conflict|mapped_elsewhere; --limit 1-1000, default 200; --offset, default 0) |
Every command takes a connector name or id. They need the org API key or an owner’s login; a member who is not an owner is refused.
Reading the counters
start, status, resume and cancel print the job’s status, its mode (dry-run or write), its scope (full or incremental after <timestamp>) and one row per entity:
| Column | Meaning |
|---|---|
Scanned | Records read from the portal |
Mapped | Mappings written; on a dry run, the mappings a real run would write |
Present | Values the identity map already holds, left untouched |
Conflicts | Of those, the ones that point at a different HubSpot record |
Ambiguous | Values held by several records, not mapped |
Skipped | Records with no identity value, with an invalid one, or not modified since --modified-after |
Under the table the CLI breaks Skipped down into its three parts, says how many records hold the ambiguous values, and reports the records that were mapped elsewhere. prewarm findings then lists them one per row — entity, kind of finding, value, HubSpot id, and what the value or the record is already mapped to:
ambiguous: deduplicate the records on the portal, then run the pre-warm again;conflict: the identity map already maps the value to another record and was left untouched;mapped_elsewhere: the record already has a mapping under another external id, shown in the last column.
A run is safe to repeat: mappings written by an earlier run are counted as Present by the next one. Starting a new pre-warm discards the findings of the previous jobs, so read them before starting again.
Job statuses, --wait and exit codes
A job is running until it ends as completed, partial_failure (at least one entity failed, for example because the HubSpot connection cannot read that object type; the others completed), failed, cancelled, superseded (a failed job replaced by a newer start) or expired.
Without --wait, start returns as soon as the job is accepted and prints the status command to follow it. With --wait (on start, status and resume) the CLI polls every 5 seconds until the job ends and exits non-zero unless the status is completed, so a cutover script can chain on it. A poll that fails with a 5xx, a 429 or a network error is retried with a warning, because the job is unaffected by it; after 12 failed polls in a row the CLI stops and says that it lost contact, not that the job failed — check it with prewarm status.
Resuming and cancelling
A job can be resumed when it failed (for example after a long HubSpot rate limit) or when it is still running but has not advanced for more than five minutes; status says so and prints the command. Only the connector’s latest job can be resumed, and never a cancelled, superseded or expired one: start a new pre-warm instead.
One pre-warm runs per connector at a time: a second start is refused with prewarm_already_running. prewarm cancel works on a queued, running or failed job. A running job stops at its next page, the mappings it already wrote stay, and it can never be resumed. It is also the way to free a connector whose job is stuck in running.
Repairing one identity mapping
Available from CLI 0.9.0.
When a record keeps being skipped with target_deleted, or keeps being written to the wrong HubSpot record, the cause is the connector’s identity mapping for that external id. Two commands repair exactly one mapping; see Repairing one identity mapping for when to use which.
# The record is gone for good, or the mapping pointed at the wrong one:
# forget the mapping, the next event finds or creates the record
plugsync connector identity remove my-connector --entity deal --external-id A1
# The record was restored on HubSpot: keep the mapping, drop the deleted marker
plugsync connector identity clear-tombstone my-connector --entity deal --external-id A1| Command | Purpose |
|---|---|
identity remove <connector> --entity <name> --external-id <id> | Remove the mapping. Shows the HubSpot record it points at before asking, and warns about any other mapping that points at the same record |
identity clear-tombstone <connector> --entity <name> --external-id <id> | Remove only the deleted-on-HubSpot marker and keep the mapping |
Both take --store-id <store> for a mapping written under a store, --reason <text> to record why, and --yes (-y) to skip the confirmation. --entity and --external-id are always required and match exactly: one call, one mapping. There is no bulk form.
Each command asks for confirmation before it changes anything. Before asking, identity remove reads the mapping and prints the HubSpot record id it is about to detach, with a reminder: if that record still exists and the entity’s identity rule does not find it, the next event for this external id creates a second record. When other mappings point at the same record it prints a warning that lists them: they are not removed, and for a record mapped by a pre-warm that matters (see Mappings written by a pre-warm). With --yes the mapping is not read first and the same warning is printed after the removal. A script must pass --yes: without it and without a terminal the command exits non-zero and removes nothing. The commands need an owner login or an organization API key, and exit non-zero with the API’s reason when the mapping does not exist (mapping_not_found), carries no marker (tombstone_not_found) or changed while the command was running (mapping_changed, safe to run again). On identity_store_unavailable the outcome is unknown: run the command again without --yes to see whether the mapping is still there.
Command reference
| Command | Purpose |
|---|---|
plugsync auth login / logout | Authenticate (email/password JWT with -e/-p, or --token ps_org_... / -t for a long-lived org API key) |
plugsync config set/get/show | Manage ~/.plugsync/config.yaml (api_url, api_key). get resolves environment variables and defaults like any command; show prints the file only, api_key masked |
plugsync pull <connector> | Download a connector’s draft as a local directory named after the connector (--revision N / -r for a published version, --output <path> / -o to choose the directory) |
plugsync push <name> | Upload a local connector directory to the draft (--publish to also publish, with --message / -m as the change summary; --force to overwrite a concurrent edit; --publish --republish to force a new version on an unchanged draft). --no-publish is still accepted but deprecated and does nothing: draft-only is the default |
plugsync diff <name> | Diff local files vs. a published version (--revision N / -r, default newest) |
plugsync validate <name> | Preflight the draft; non-zero exit on an error (--push to validate local files) |
plugsync preview <name> | Preview a publish’s effects and its preflight findings |
plugsync log [connector] | Show a connector’s published version history (--limit N / -n, default 20); without an argument, the connector of the current directory |
plugsync rollback <version> [connector] | Restore a previous revision into the draft; without a connector, the one of the current directory |
plugsync connector poll <connector> | Poll the connector’s poller, scheduled and file sources now (--source <name> / -s for one source) |
plugsync connector breakers <name> | List the connector’s flows whose circuit breaker is open |
plugsync connector flow retry <name> <flow> | Retry one blocked flow now, instead of waiting for its automatic attempt |
plugsync connector reauthorize <name> | Print the HubSpot link that re-authorizes the connected portal (--scopes a,b to add the scopes a preflight finding lists) |
plugsync connector prewarm start/status/resume/cancel/findings <name> | Fill the connector’s identity map from the records already on its HubSpot portal, before a migration cutover |
plugsync connector identity remove/clear-tombstone <name> --entity <entity> --external-id <id> | Repair one identity mapping: forget it, or clear its deleted-on-HubSpot marker |
plugsync plugin init/push/list/status/logs/metrics/invoke/promote/publish | Author and manage plugins (plugin rollback exists but is not available yet: it exits with an error and changes nothing) |
<connector> is a connector name or id (see Naming the connector); push, diff, validate and preview take a connector directory.
Run plugsync --help or plugsync <command> --help for the full option list.
Plugins
The plugsync plugin command group is the authoring surface for the plugin runtime: scaffold with init, deploy with push, inspect with status/logs/metrics, test with invoke, and move a build through environments with promote (or its alias publish). The full walkthrough, including what happens server-side on every push, is the plugin dev loop.
push uploads the bundle and queues a deploy to your own dev function. The upload itself leaves the plugin’s promotion state alone, which is why its output prints the state the plugin had at that moment (status, live_version, staging_version) separately from the upload. Once the dev deploy completes, status reads dev and the plugin’s function ARN points at your dev function, even on a plugin that was promoted before: live_version and staging_version stay as they were, and live connectors keep running the build that was promoted. The deploy runs asynchronously (“it may take a few seconds”); follow it with plugin status.
| Command | Options |
|---|---|
plugin init [name] | Scaffolds src/index.ts, plugin.json and package.json locally. No API call |
plugin push [path] | Bundles src/index.ts with esbuild (which must be on your PATH) and uploads it; creates the plugin first when plugin.json has no id |
plugin promote <id> staging|live | Promotes the last pushed bundle. live asks for confirmation |
plugin publish <id> [staging|live] | Alias of promote; the target defaults to staging |
plugin logs <id> | --connector <connector> keeps only one connector’s lines (name or id); --since <minutes> (default 60, max 1440); --limit <n> (default 50, max 1000) |
plugin metrics <id> | --window 1h|24h|7d (default 24h) |
plugin invoke <id> | --payload / -p (default {}), --config / -c, --auth / -a, --event-type / -e, described below |
invoke calls the function recorded on the plugin, which follows the latest deploy: after a push that is your dev function, after a promotion the shared one. It fills the whole plugin envelope, not only the event body: --payload is event.payload, --config <json> is event.config, --event-type <type> is event.event_type, and --auth <source-name> resolves that event source’s outbound credential into headers at event.auth[<source-name>] exactly as a published flow would, subject to the plugin’s auth:<source-name> capability grant.
plugin rollback is not available yet: it changes nothing, says so and exits with code 1, so a script that runs it does not take it as done. To go back to an earlier build, push its source again with plugin push and promote it with plugin promote.
Three plugin operations require an org tier with the Plugin System (Enterprise and above): creating a plugin, pushing a bundle and promoting it. Without it they return 402 Payment Required, and plugin push prints your current tier and an upgrade link. The other plugin commands (list, status, logs, metrics, invoke) are not gated, plugin init never calls the API, and the connector-as-code commands above have no tier gate. What each plan includes: Plan limits.
Compatibility
The CLI declares the minimum API version it expects and checks it against GET /api/version at the start of any command that talks to the API, warning (never failing outright) if the API is older than expected. An API that answers but does not expose GET /api/version is reported the same way: it predates the version endpoint, so compatibility cannot be verified and the warning names the minimum version this CLI expects.
License
The CLI is proprietary, not open source. The license to install and use it
is granted by the plugsync
Terms of Service . The LICENSE file shipped
with the package carries the full notice.