SalesOS

CLI

salesos — sign in, call any endpoint, deploy and test metadata, listen to live events, work with functions, records, bulk jobs and developer orgs from the terminal.

The salesos command line is built on the TypeScript SDK, so it follows the same retry, idempotency and pagination rules. It needs Node 18+.

The CLI is not on npm yet. Build it from the SalesOS repository: build packages/sdk-js, then cd packages/cli && npm install && npm run build && npm link.

Sign in

Three ways, stored per profile:

# API key (prompted, hidden, when you leave the value off)
salesos login --api-key

# OAuth: authorization code + PKCE with a loopback redirect
salesos login --client-id sos_ci_… --scope "openid email offline_access READ_LEADS WRITE_LEADS"

# Web session: developer orgs, OAuth app management, anything an admin does in the app
printf '%s' "$PASSWORD" | salesos login --email [email protected] --password-stdin
salesos login --email [email protected] --code 123456   # 2FA code up front (or SALESOS_2FA_CODE)

For OAuth, register a public app under Settings → OAuth apps with the redirect URI http://127.0.0.1:8976/callback and the scopes you will ask for. SalesOS matches redirect URIs exactly, port included, so pass --port if you registered a different one. The CLI opens your browser (--no-browser just prints the URL), waits for the redirect on 127.0.0.1, checks state and the issuer, and exchanges the code. Ask for offline_access to get a refresh token; the CLI refreshes before expiry and saves every rotated refresh token. Without --scope it asks for openid profile email offline_access plus WRITE_ on every core resource, so the app must allow all of those.

Session sign-in handles two-factor authentication: when your organization asks for a code, the CLI prompts for a TOTP or backup code (or takes --code / SALESOS_2FA_CODE). If your account has not enrolled yet, enroll in the web app first. A session renews itself (POST /v1/auth/refresh) whenever a command runs in the last 20 minutes of its hour, and saves the rotated token, so it lasts as long as you keep using it; after an hour without a command, log in again. Two salesos processes sharing a profile pick up each other's rotations.

salesos whoami            # profile, base URL, credential type (never the secret), user
salesos logout            # revokes OAuth tokens / ends the session, then forgets them

Profiles and config

--profile <name> (or -p, or SALESOS_PROFILE) picks a profile; the first one you log in to becomes the default. Credentials live in ~/.config/salesos/config.json ($XDG_CONFIG_HOME is honoured, SALESOS_CONFIG overrides the path). The file is written atomically with mode 600 in a 700 directory, and a looser file is tightened when it is read. The CLI never prints keys or tokens.

For CI, skip the config: SALESOS_API_KEY (or --api-key KEY) and SALESOS_TOKEN (a bearer token) take precedence over the profile. --base-url / SALESOS_BASE_URL point at another deployment.

Records

salesos records list leads --filter status=NEW --search acme --limit 50
salesos records list deals --filter owner=me --filter open=true --all --columns id,name,amount,stage
salesos records get contacts cmc_123 --json
salesos records create leads --data '{"firstName":"Ada","lastName":"Lovelace","company":"Analytical"}'
salesos records update opportunities cmo_9 --data @patch.json
salesos records delete tasks cmt_1 --yes
salesos records list custom:<objectId> --all

Objects: leads, contacts, accounts (companies), opportunities (deals), tasks, quotes, orders, activities, cases, and custom:<objectId> for custom object records. --filter key=value passes any filter the endpoint accepts (see Requests); --all walks every page and --max N stops early. --data takes JSON, @file or - for stdin.

Any endpoint

salesos api GET /v1/pipelines
salesos api POST /v1/leads/cml_1/convert --data '{"createOpportunity":true}'
salesos api GET /v1/opportunities -q stage=PROSPECTING -q limit=5 -H "X-Organization-ID: org_…"

The status and request id go to stderr, the body to stdout (--out writes it to a file). POST/PATCH get an automatic Idempotency-Key; pass --idempotency-key to choose it.

Output

Lists print as tables and single records as key/value blocks. Add --json to any command for machine-readable output:

salesos records list leads --all --json | jq -r '.[].email'

Exit codes: 0 success, 1 the API or the job failed, 2 usage error. --verbose logs retries.

Platform

salesos openapi pull --out openapi.json      # the live OpenAPI document (or --out - for stdout)
salesos events list --category Deals           # the event catalog
salesos events get opportunity.won

salesos webhooks list
salesos webhooks create --url https://hooks.example.com/salesos --events lead.created,opportunity.won
salesos webhooks test <id> --type opportunity.won
salesos webhooks deliveries <id>

salesos actions list
salesos actions test <id> --input '{"domain":"acme.example"}'

webhooks create writes the signing secret, which SalesOS shows only once, to webhook-<id>.secret (mode 600, or --secret-out <file>) instead of printing it.

Verify a delivery locally

salesos webhooks verify \
  --signature 't=1790000000,v1=7753…' \
  --body @payload.json \
  --secret "$SALESOS_WEBHOOK_SECRET"

Exits 0 when the signature is valid and 1 when it isn't. The body is read byte for byte, so save it exactly as received. --tolerance <seconds> changes the 300-second window and --ignore-time skips it when you replay an old capture.

Bulk data

salesos bulk export contacts --fields email,firstName,lastName --format csv --out contacts.csv
salesos bulk export leads --filter '{"status":"NEW"}' --format jsonl > new-leads.jsonl
salesos bulk import contacts contacts.csv --operation upsert --external-id email
salesos bulk status <jobId>

Jobs run on the Bulk API. The CLI polls and prints progress to stderr. An import with failed rows writes them to bulk-<jobId>-failures.csv (or --failures-out) and exits 1. --no-wait returns as soon as the job is queued.

Developer orgs

salesos devorg create "Integration playground"
salesos devorg list
salesos devorg open <id>     # this profile now works inside the developer org
salesos devorg leave         # back to the org it came from
salesos devorg delete <id> --yes

These need a session profile (login --email), because the API refuses keys and OAuth tokens on these routes. open and leave switch the profile's session. See Developer orgs for limits.

Metadata

salesos project init my-crm             # salesos.project.json + src/
salesos metadata pull [--types FUNCTION,WORKFLOW]
salesos metadata diff [--target production]
salesos metadata push --check-only      # validate against the active profile, write nothing
salesos metadata push [--activate] [--run-tests NONE|SPECIFIED|ALL]   # deploy to the active profile
salesos metadata deploy --target production [--check-only] [--run-tests …] [--delete-missing --yes] [--destructive file.json]
salesos metadata deploys                # history
salesos metadata status <deployId>

push deploys to the active profile; deploy --target deploys the same source to another profile, so -p devorg … deploy --target production promotes what you built in a developer org. Both print progress and a result per component and exit 1 when the deploy fails. diff compares JSON by value (key order and spacing don't count) and prints changed lines. See Metadata as code for the format and deploy rules.

--types takes any type the server lists in GET /v1/metadata/types: the CLI knows every folder (objects, fields, validation-rules, workflows, actions, hooks, functions, layouts, pages, app-pages, flows, approval-processes, labels, translations, profiles, reports, dashboards, email-templates, list-views, assignment-rules, credentials, events, after-save). A push or deploy runs the package's tests first (--run-tests SPECIFIED, the default); a failing test fails it.

Test

salesos test                              # the project's tests, against the active profile
salesos test --target production          # …against another profile
salesos test --run-tests ALL              # plus the tests earlier deploys left in the org
salesos test --json > test-results.json   # for CI

test sends the project as a check-only deploy with tests: the server validates the package against the org and runs every test file in it — functions and flows with every outside call mocked, validation rules and formulas exactly as a save evaluates them. Nothing is written. It prints one row per test (status, kind, target/name, time, the reason a test failed) and exits 0 when everything passed, 1 when a test failed or the package doesn't validate, 2 on a usage error. --json prints { deployId, status, ok, runTests, total, passed, failed, durationMs, tests[], problems[] }. The test file format is in Testing.

.github/workflows/metadata.yml
- run: salesos test --target production --json > results.json
  env:
    SALESOS_API_KEY: ${{ secrets.SALESOS_PROD_KEY }}   # a key with MANAGE_METADATA

Listen to live events

salesos listen --forward-to http://localhost:3000/webhooks --events lead.created,opportunity.*
salesos trigger lead.created            # a catalog sample, flagged "test": true
salesos trigger lead.created --webhook <id>   # the webhook test delivery instead

listen streams the org's catalog events over POST /v1/webhooks/listen (Server-Sent Events) and POSTs each one to --forward-to exactly as a webhook would arrive: the same envelope body, byte for byte, and the same X-SalesOS-Event, X-SalesOS-Event-Id and X-SalesOS-Signature: t=…,v1=… headers. The signature uses a secret the CLI generates for this session and prints once — verify with it locally the way you will verify production deliveries. It never leaves your machine. Without --forward-to, events are printed as JSON lines.

Listening is equivalent to owning a webhook for the whole org: org owner/admin, and WRITE_WEBHOOKS for keys and tokens. A stream ends after 15 minutes and the CLI reconnects (with refreshed credentials); events published during the second it takes are not replayed, and a terminal that falls far behind is told how many events it dropped. Up to 5 listen sessions per org. trigger sends only to listen sessions — never to your real webhooks.

Functions

salesos functions init score_lead [--language javascript]
salesos functions list
salesos functions deploy score_lead [--activate] [--check-only] [--target production]
salesos functions invoke score_lead --input '{"leadId":"…"}'
salesos functions logs score_lead --follow [--status FAILED] [--no-output]
salesos functions secrets score_lead set ENRICH_TOKEN --value-stdin
salesos functions secrets score_lead list       # names only; values are write-only
salesos functions secrets score_lead delete ENRICH_TOKEN

init writes src/functions/<apiName>/ in your project. deploy sends just that folder through the metadata deploy, so the rules match (compiled on the server, inactive unless --activate, missing secrets reported). logs --follow polls the invocation log and prints each run with its captured stdout/stderr.

On this page