An agent-first CLI for Customer.io APIs.
800+ Journeys routes + 100+ CDP Pipelines routes, zero per-endpoint code. A single cio api <path> command covers every endpoint. Every command returns structured JSON to stdout. Every error returns structured JSON to stderr.
AI agents are the primary consumer. Use cio schema to introspect endpoints before calling them.
npm i -g @customerio/cli
cio --helpTo build from source instead:
go install github.com/customerio/cli@latestThis repo ships a SKILL.md so Claude Code, Cursor, Codex, Windsurf, and other agents that support open agent skills know how to drive the CLI. Install it with:
npx skills add customerio/cliThe CLI uses service account tokens (sa_live_...) for authentication. These are exchanged for short-lived JWTs via OAuth 2.0 client credentials, just like gh auth.
# Interactive — prints the browser login URL, then prompts for the minted token
cio auth login
# Read from stdin (for CI/automation; login still auto-discovers region)
echo "$SA_TOKEN" | cio auth login --with-token
# Verify auth works
cio auth status
# Print raw token
cio auth token
# Logout
cio auth logout--tokenflag (highest priority)CIO_TOKENenvironment variable~/.cio/config.jsonfile (lowest priority)
When you use CIO_TOKEN or --token directly on normal commands, you may
also need CIO_REGION=us|eu or --api-url.
- You provide a
sa_live_...token (from Customer.io UI → Account Settings → Manage API Credentials → Service Accounts) - The CLI exchanges it for a JWT via
POST /v1/service_accounts/oauth/token - The JWT is cached locally and refreshed automatically when it expires
- All API calls use
Authorization: Bearer <jwt>
cio --token sa_live_xxx api /v1/environments/{environment_id}/campaigns --params '{"environment_id": "123"}'
CIO_TOKEN=sa_live_xxx cio api /v1/environments/{environment_id}/campaigns --params '{"environment_id": "123"}'Use cio api <path> for any API endpoint. Path placeholders are resolved from --params. The HTTP method defaults to GET (or POST if --json is provided); override with -X:
# List campaigns in workspace 123
cio api /v1/environments/{environment_id}/campaigns --params '{"environment_id": "123"}'
# Get a specific campaign
cio api /v1/environments/{environment_id}/campaigns/{campaign_id} \
--params '{"environment_id": "123", "campaign_id": "456"}'
# Create a campaign
cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' \
--json '{"campaign": {"name": "Welcome Flow", "type": "none"}}'
# Explicit method override
cio api /v1/environments/{environment_id}/campaigns/{campaign_id} -X DELETE \
--params '{"environment_id": "123", "campaign_id": "456"}'
# Introspect endpoints
cio schema # list all resources
cio schema campaigns # list endpoints for a resource
cio schema campaigns.list # full schema for a method
cio schema GET /v1/environments/{environment_id}/campaigns # by HTTP method + pathPaths that include {account_id} auto-fill from the account ID stored during cio auth login, so you don't need to pass it on every call:
cio api /v1/accounts/{account_id}/environmentsPass --params '{"account_id": "..."}' to override, or set CIO_ACCESS_TOKEN to disable the fallback (the pre-exchanged JWT may belong to a different account).
# Filter with --jq to save context window
cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' \
--jq '.campaigns[] | {id, name, state}'
# Complex filtering
cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' \
--jq '.campaigns[] | select(.state == "active") | {id, name}'# Always dry-run first
cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' \
--json '{"campaign": {"name": "Welcome Flow", "type": "none"}}' --dry-run
# Then execute (removes --dry-run)
cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' \
--json '{"campaign": {"name": "Welcome Flow", "type": "none"}}'cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' --page 2 --limit 50
# Auto-paginate (emits NDJSON — one JSON object per line)
cio api /v1/environments/{environment_id}/campaigns \
--params '{"environment_id": "123"}' --page-all| Flag | Env Var | Description |
|---|---|---|
--token <value> |
CIO_TOKEN |
Service account token override |
-X, --method |
HTTP method override (default: GET, or POST if --json) | |
--json <payload> |
Raw JSON request body or @filename to read from file |
|
--params <json> |
Query parameters as JSON → query string | |
--jq <expr> |
jq expression filter (via gojq) | |
--dry-run |
Validate + print request, skip execution | |
--api-url <url> |
API base URL override | |
--timeout <duration> |
CIO_TIMEOUT |
HTTP request timeout (default: 30s) |
--page <n> |
Page number | |
--limit <n> |
Page size | |
--page-all |
Auto-paginate, emit NDJSON |
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Validation / input error |
| 3 | Authentication error |
| 4 | Authorization error |
| 5 | API error (4xx/5xx) |
{"error":true,"code":"AUTH_ERROR","message":"Not authenticated.","details":{"status_code":401}}The CLI uses a generic api command + route registry architecture:
cio api <path>— a single command that takes any API path with{placeholder}params- OpenAPI specs are downloaded from the live API on first use and cached locally under
~/.cio/cache/specs/(24h TTL, ETag-based conditional refresh). Usecio schema --refreshto force re-download. internal/routes/enrichment.json— summaries, param descriptions, query params for routes not yet annotated in the OpenAPI spec
# Discover resources
cio schema
# List endpoints for a resource
cio schema campaigns
# Inspect a method's parameters
cio schema campaigns.list
# Make an API call
cio api /v1/environments/{environment_id}/campaigns --params '{"environment_id": "123"}'
# CDP Pipelines (workspace_id = environment_id)
cio api /cdp/api/workspaces/{workspace_id}/sources --params '{"workspace_id": "123"}'
cio schema sourcesgo build -o cio .
go test ./...
go test ./... -v -run TestAuthLoginLicensed under Apache License 2.0 with the Commons Clause Restriction. See LICENSE.