# Cloudzero CLI

> Invoke the `cloudzero` CLI to drive the CloudZero API — billing costs/dimensions, billing connections (AWS/Azure/GCP/AnyCost), budgets, CostFormation definition versions/namespaces/publish-jobs, insights + comments, optimization recommendations + comments + recommendation types, roles + permission sets, views, engineering events, telemetry (allocation + unit metric streams), and internal dashboard endpoints (cost data over time, grouped costs, clusters, groups, SSO integrations, notifications, Looker reports, realized savings). Uses the public key-authenticated API (hand-crafted commands) plus internal dashboard (Bearer JWT) commands generated from captured HARs, hand-curated internal endpoints, and a `raw` escape hatch for any un-codified path. Use whenever a task needs CloudZero data or actions, such as "list our insights", "get billing costs", "create a budget", "send telemetry", or "list recommendations".

- Skill: `paymog/cloudzero-cli` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paymog/cloudzero-cli`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paymog/cloudzero-cli/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: paymog (https://skillmd.com/u/paymog)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paymog/cloudzero-cli

---


# CloudZero CLI

Invoke the `cloudzero` binary. Source of truth: [`paymog/cloudzero-cli`](https://github.com/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.

```sh
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

```sh
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>`:

```sh
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`.

```sh
# 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
```sh
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)
```sh
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
```sh
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
```sh
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
```sh
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)
```sh
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
```sh
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
```sh
# 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)
```sh
# 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)
```sh
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)
```sh
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:

```sh
# 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:

```sh
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:

```sh
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):**
1. Probe with an empty/partial body: `cloudzero raw POST /organizations/<org>/budgets --service budgets --body-json '{}'`.
2. Read the `400`/`422` validation error — it names the required fields; retry with invalid enum
   values to learn allowed values.
3. Add a `Command` to `src/commands/manual-internal.ts` (it's merged at load time and survives HAR
   regeneration), then `bun run build`.

## Recipes
### Validate your API key
```sh
cloudzero billing dimensions | jq '.dimensions | length'
```

### Get cost data grouped by service
```sh
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)
```sh
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
```sh
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?
```sh
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):

```sh
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.

