CloudZero CLI
Invoke the cloudzero binary. Source of truth: paymog/cloudzero-cli.
CloudZero has a public API keyed by API keys. The Authorization header carries the raw key
(NOT Bearer). cloudzero speaks it directly — no browser, no session token. Commands are hand-crafted
from CloudZero's API reference (docs.cloudzero.com/reference/). The API base is
https://api.cloudzero.com (paths: /v2/*, /events/v1/*, /unit-cost/v1/*).
Auth (required before any command)
An API key. Create one at Settings → API Keys (app.cloudzero.com/organization/api-keys);
when creating it you choose its scopes (e.g. billing:read_costs, insights:read_insights), and
the scope set is fixed after creation.
cloudzero auth set <api-key> # store it (chmod 600, ~/.config/cloudzero/creds.json)
cloudzero auth status # show masked key + last-updated
cloudzero auth logout # clear stored credentials
Resolution order: --api-key <key> flag → $CLOUDZERO_API_KEY → stored credential. For a one-off
or CI, export CLOUDZERO_API_KEY and skip auth set.
A 401/403 means the key is invalid, revoked, or lacks the scope the endpoint needs. Re-check
the key's scopes in the dashboard.
Usage
cloudzero list [filter] # every command (optionally filtered by substring)
cloudzero list insights # all verbs for the `insights` resource
cloudzero <resource> <verb> [flags]
Commands are two tokens: <resource> <verb>. Output is pretty-printed JSON — pipe to jq,
or pass --raw for the unformatted response.
Flags
| Flag | Meaning |
|---|---|
--api-key <key> |
API key for this call (else $CLOUDZERO_API_KEY or stored) |
--<param> <value> |
path params: --id, --budget-id, --connection-id, --insight-id |
--query key=value |
query param, repeatable (incl. repeat keys like status=foo&status=bar) |
--body-file <path> |
JSON request body from file (for POST/PUT/PATCH) |
--body-json '<json>' |
inline JSON request body |
--set a.b=value |
set a body field, repeatable |
--raw |
print the raw response, no JSON formatting |
--auth token|key |
(raw only) force the auth mode; otherwise inferred from the path |
--service <name> |
(raw only) service subdomain for internal calls (e.g. billing, identity) |
Pagination
List responses include a pagination root key with cursor.next_cursor / cursor.previous_cursor.
Pass the URL-encoded cursor back via --query cursor=<cursor>:
cloudzero insights list --query limit=250 > first.json
AFTER=$(jq -r '.pagination.cursor.next_cursor // empty' first.json)
[ -n "$AFTER" ] && cloudzero insights list --query limit=250 --query "cursor=$AFTER" > second.json
Filtering & sorting
List endpoints return filtering and sorting root keys describing available fields, valid values,
and sort keys. Pass filters as repeatable --query params; sorting via --query sort_key=<field>
and --query sort_order=asc|desc.
# insights in "new" and "in_progress" status, sorted by cost impact descending
cloudzero insights list --query status=new --query status=in_progress --query sort_key=cost_impact --query sort_order=desc
Command surface
Run cloudzero list for the authoritative set (~100 commands: ~70 public API commands plus ~30
internal/dashboard commands marked 🍪, and the raw escape hatch). Grouped highlights (GET unless
noted):
Billing
cloudzero billing costs --query start_date=2026-07-01 --query granularity=daily --query cost_type=real_cost
# Required: start_date (YYYY-MM-DD or ISO), granularity (hourly|daily|weekly|monthly|yearly).
# Optional: group_by (repeatable: Account, Service, CZ:Defined:ServiceDetail, User:Defined:AccountName, ...),
# cost_type (real_cost), filters (JSON object), limit, cursor.
# Rate limit: 60 req/day, 30s timeout. Paginated in blocks of 10,000.
cloudzero billing dimensions # list available dimensions for grouping/filtering
Connections (billing)
cloudzero connections list
cloudzero connections create --body-json '{...}'
cloudzero connections show --connection-id <id>
cloudzero connections update --connection-id <id> --body-json '{...}' # AnyCost bucket only
cloudzero connections delete --connection-id <id> # not AWS/Snowflake
cloudzero connections anycost-drops --connection-id <id>
cloudzero connections anycost-drop-create --connection-id <id> --body-json '{...}' # max 5MB
cloudzero connections anycost-drop-contents --connection-id <id> --month 2026-07
cloudzero connections anycost-validate --body-json '{...}'
Budgets
cloudzero budgets list
cloudzero budgets create --body-json '{...}'
cloudzero budgets show --budget-id <id>
cloudzero budgets update --budget-id <id> --body-json '{...}'
cloudzero budgets delete --budget-id <id>
CostFormation
cloudzero costformation versions
cloudzero costformation version-create --body-json '{...}' # max 10MB
cloudzero costformation version-show --version <version>
cloudzero costformation publish-async --body-json '{...}' # async publish job
cloudzero costformation publish-jobs
cloudzero costformation publish-job-show --job-id <id>
cloudzero costformation publish-job-cancel --job-id <id>
cloudzero costformation namespaces
cloudzero costformation namespace-create --body-json '{...}'
cloudzero costformation namespace-show --id <id>
cloudzero costformation namespace-update --id <id> --body-json '{...}' # edit_role_ids ⊆ view_role_ids
cloudzero costformation namespace-delete --id <id>
Insights
cloudzero insights list [--query status=new] [--query status=in_progress] [--query sort_key=cost_impact] [--query sort_order=desc]
cloudzero insights create --body-json '{...}'
cloudzero insights show --insight-id <id>
cloudzero insights update --insight-id <id> --body-json '{...}'
cloudzero insights delete --insight-id <id>
cloudzero insights comments --insight-id <id>
cloudzero insights comment-create --insight-id <id> --body-json '{...}'
cloudzero insights comment-update --insight-id <id> --comment-id <id> --body-json '{...}'
Recommendations (Optimize)
cloudzero recommendations list [--query status=not_started] [--query status=in_progress] [--query sort_key=cost_impact_last_30_days]
cloudzero recommendations update --body-json '{...}' # bulk update: status, effort, work item
cloudzero recommendations show --recommendation-id <id>
cloudzero recommendations comments --recommendation-id <id>
cloudzero recommendations comment-create --body-json '{...}' # one or more recommendations
cloudzero recommendations comment-delete --comment-id <id>
cloudzero recommendations comment-update --comment-id <id> --body-json '{...}'
cloudzero recommendation-types list
cloudzero recommendation-types show --id <id>
cloudzero recommendation-types update --body-json '{...}'
Roles & views
cloudzero roles list [--query name=<name>] [--query sort_key=name]
cloudzero roles create --body-json '{...}' # name, description, permission_sets
cloudzero roles show --role-id <id>
cloudzero roles update --role-id <id> --body-json '{...}'
cloudzero roles delete --role-id <id>
cloudzero roles permission-sets
cloudzero views list
cloudzero views create --body-json '{...}'
cloudzero views show --view-id <id>
cloudzero views update --view-id <id> --body-json '{...}'
cloudzero views delete --view-id <id>
Events
# Post an engineering event for cost correlation (deployments, incidents, etc.)
cloudzero events create --body-json '{
"title": "production-feature-deployed",
"source": "jenkins",
"timestamp": "2026-07-13T15:32:44.474670+00:00",
"filters": {"accounts": ["012093365392"], "services": ["AWSLambda"]},
"link": "http://jenkins.com/builds/production-feature",
"metadata": {"user": "sasha@company.com", "git_sha": "923dd0037a"}
}'
Telemetry (allocation)
# Submit allocation telemetry (splits cloud cost via custom allocation dimensions)
cloudzero telemetry allocation-sum --telemetry-stream-name my-stream --body-json '{
"records": [{
"value": 16.4,
"timestamp": "2026-07-13T15:32:44+00:00",
"granularity": "HOURLY",
"element_name": "tenant-a",
"filter": {"services": ["AmazonEC2"]}
}]
}'
cloudzero telemetry allocation-replace --telemetry-stream-name <name> --body-json '{...}'
cloudzero telemetry allocation-replace-period --telemetry-stream-name <name> --body-json '{...}'
cloudzero telemetry allocation-delete --telemetry-stream-name <name> --body-json '{...}'
cloudzero telemetry allocation-post --telemetry-stream-name <name> --body-json '{...}' # legacy
Telemetry (unit metric)
cloudzero telemetry metric-sum --metric-name my-metric --body-json '{...}'
cloudzero telemetry metric-replace --metric-name <name> --body-json '{...}'
cloudzero telemetry metric-replace-period --metric-name <name> --body-json '{...}'
cloudzero telemetry metric-delete --metric-name <name> --body-json '{...}'
cloudzero telemetry metric-records --metric-name <name> [--query limit=100]
cloudzero telemetry metric-post --metric-name <name> --body-json '{...}' # legacy
Telemetry (stream management)
cloudzero telemetry stream-records --telemetry-stream-name <name>
cloudzero telemetry stream-delete --telemetry-stream-name <name> [--body-json '{"force":true}']
Dashboard / internal API (🍪 — needs a browser session)
These hit *.discovery.cloudzero.com, which reject API keys (401 Unauthorized). They require
a Bearer JWT from a logged-in browser session (short-lived, ~2h). Import one first:
# devtools → Network → right-click any *.discovery.cloudzero.com request → Copy as cURL
cloudzero auth import '<paste curl>' # or: pbpaste | cloudzero auth import
cloudzero auth set-org 85401732-d2cd-4e29-ba68-ade774a540dd # org UUID (auto-captured from the curl URL if present)
Then commands marked 🍪 in list work. Highlights — things the public API can't do:
cloudzero billing cost-data-over-time --cost-data-over-time-id <view-id> 🍪 (billing)
--query partition_by=services --query granularity=daily --query start_date=2026-07-01 --query end_date=2026-07-13
cloudzero billing grouped-costs --grouped-costs-id <view-id> 🍪 (billing)
cloudzero billing partition-values --partition-values-id <view-id> 🍪 (billing)
--query partition=k8s_namespace --query limit=10000 --query relative_time_span=last_30_days
cloudzero connections list 🍪 (billing-connections)
--query expand=ingest_health --query expand=statistics --query sort_key=creation_date --query sort_order=desc
cloudzero budgets list 🍪 (budgets)
--query limit=1000 --query expand=current
cloudzero clusters list 🍪 (container-analysis)
cloudzero groups list 🍪 (data-access-controls)
cloudzero groups show --group-id <id> 🍪 (data-access-controls)
cloudzero identity api-keys 🍪 (identity)
cloudzero identity api-scopes 🍪 (identity)
cloudzero identity org-email-invitations 🍪 (identity)
cloudzero identity integrations 🍪 (identity) # SSO integrations
cloudzero insights list 🍪 (insights)
--query limit=10000 --query sort_key=cost_impact --query sort_order=desc
cloudzero recommendations list 🍪 (insights-workflow)
--query status=not_started --query status=in_progress --query sort_key=cost_impact_last_30_days
cloudzero realized-savings list 🍪 (insights-workflow)
--query start_date=2026-06-13 --query end_date=2026-07-14
cloudzero reports folders 🍪 (looker)
cloudzero reports folders show --folder-id <id> 🍪 (looker)
cloudzero scheduledPlans list 🍪 (looker)
cloudzero channel-configurations list 🍪 (notifications)
cloudzero channels week 🍪 (notifications)
cloudzero install-slack-url list 🍪 (notifications)
cloudzero integrations list --query provider=jira 🍪 (ticket-integrations)
cloudzero unit-cost streams 🍪 (unit-cost)
cloudzero preferences list 🍪 (user-session-gateway)
cloudzero preferences update --body-json '{...}' 🍪 (user-session-gateway)
The :organization_id path param is auto-filled from stored creds (--org → $CLOUDZERO_ORG_ID
→ stored) — you don't pass it explicitly. Other path params (e.g. --connection-id, --group-id)
are required.
Raw requests & reverse-engineering new endpoints
Not every endpoint is codified. cloudzero raw <METHOD> </path> hits any endpoint with your
stored creds — the fast path for probing and reverse-engineering internal routes:
cloudzero raw GET /v2/insights --query limit=1 # key inferred (not /organizations/*)
cloudzero raw GET /organizations/<org-id>/clusters --service container-analysis # token inferred
cloudzero raw POST /organizations/<org-id>/budgets --service budgets --body-json '{}' # probe: 400/422 names required fields
cloudzero raw GET /v2/billing/costs --query start_date=2026-07-01 --query granularity=daily
Auth is inferred (/organizations/* → token, otherwise key); override with --auth token|key.
For token auth, --service <name> is required (names the *.discovery.cloudzero.com subdomain).
Inline any IDs directly in the path (raw does no :param substitution).
Codify a new endpoint (recipe):
- Probe with an empty/partial body:
cloudzero raw POST /organizations/<org>/budgets --service budgets --body-json '{}'. - Read the
400/422validation error — it names the required fields; retry with invalid enum values to learn allowed values. - Add a
Commandtosrc/commands/manual-internal.ts(it's merged at load time and survives HAR regeneration), thenbun run build.
Recipes
Validate your API key
cloudzero billing dimensions | jq '.dimensions | length'
Get cost data grouped by service
cloudzero billing costs \
--query start_date=2026-07-01 \
--query granularity=daily \
--query group_by=Service \
--query cost_type=real_cost \
| jq '.costs[] | {service: .partition, cost: .cost}'
Page through insights (cursor pagination)
cloudzero insights list --query limit=250 > first.json
AFTER=$(jq -r '.pagination.cursor.next_cursor // empty' first.json)
[ -n "$AFTER" ] && cloudzero insights list --query limit=250 --query "cursor=$AFTER" > second.json
Send an allocation telemetry record
cloudzero telemetry allocation-sum --telemetry-stream-name requests-per-tenant --body-json '{
"records": [{
"value": 15420,
"timestamp": "2026-07-13T15:00:00+00:00",
"granularity": "HOURLY",
"element_name": "tenant-a",
"filter": {"services": ["AmazonEC2"]}
}]
}'
Which verbs does a resource have?
cloudzero list insights # shows list/create/show/update/delete + comments/comment-create/comment-update
cloudzero list telemetry # shows allocation-sum/post/replace/... + metric-* + stream-*
Regenerate the command catalog
Internal commands are generated from captured browser HAR(s):
bun run codegen:har ~/Downloads/app.cloudzero.com.har # one or more HARs
It harvests GET and write endpoints (POST→create, PUT/PATCH→update, DELETE→delete), templates
UUIDs to :organization_id (first UUID) or <segment>_id (subsequent UUIDs), and overwrites
generated-internal.ts — so pass every HAR you want represented in a single invocation. Hand-verified
internal endpoints that appear in no HAR live in src/commands/manual-internal.ts and survive
regeneration.
Common issues
not authenticated
No key found via flag, env, or store. Run cloudzero auth set <key> or export CLOUDZERO_API_KEY=<key>.
HTTP 401 / HTTP 403 (public/key commands)
The key is invalid/expired, or lacks the scope the endpoint requires. Check the key's scopes in Settings → API Keys; scopes are fixed at creation — create a new key if you need more.
Dashboard commands (*.discovery.cloudzero.com) reject API keys. They need a Bearer JWT from a
logged-in browser session: re-import via cloudzero auth import <curl> (Copy-as-cURL from
app.cloudzero.com), and ensure the org id is set (auth set-org or --org). The JWT is
short-lived (~2h) and may be revoked server-side even before expiry — re-import a fresh one.
HARs from Chrome/Brave usually strip the JWT — use Copy-as-cURL.
HTTP 422 / HTTP 400 with a validation message
The body/query is the wrong shape (missing required field, bad enum, wrong type). The error body
names the offending field — fix the --body-json/--query/--set value.
billing costs returns 429
Rate limit: 60 requests/day for /v2/billing/costs. Back off and retry; don't hammer. Using cursor
pagination doesn't consume additional rate-limit budget.
unknown command
Commands are <resource> <verb>. Run cloudzero list <resource> to see the exact verbs. If you
typed just the resource, the CLI suggests its verbs. CRUD verbs are collapsed
(insights list/create/show/update/delete); non-CRUD actions keep their name
(insights comment-create, telemetry allocation-sum).
Missing endpoint
The endpoint isn't in the catalog. Use cloudzero raw <METHOD> </path> to probe it, then codify it
in src/commands/manual-internal.ts. Re-run bun run codegen:har to pick up new HAR-captured
internal endpoints, then rebuild.