> ## Documentation Index
> Fetch the complete documentation index at: https://docs.integrateledger.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI

> Keep your catalog in your repo as code, and every ledger command, flag, exit code and JSON output.

The `ledger` CLI keeps your catalog in your repository, as `ledger.config.ts` or `ledger.catalog.json`. You change pricing by editing the file, review the change in a pull request, and publish it with `ledger push`. [Setup](https://docs.integrateledger.com/getting-started/setup.md) walks through the first run.

Install it with `npm install --save-dev integrateledger-cli` and run `npx ledger`, or run `npx integrateledger-cli` without installing. It needs Node 22.18 or later, or Bun.

## The config file

```ts ledger.config.ts theme={"system"}
import { creditPack, defineCatalog, feature, plan } from "integrateledger-cli";

export default defineCatalog({
	plans: [
		plan({ id: "free", name: "Free", monthlyCredits: 20, monthlyPriceCents: 0 }),
		plan({ id: "pro", name: "Pro", monthlyCredits: 1000, monthlyPriceCents: 2900 }),
	],
	features: [
		feature({ id: "generate_image", name: "Generate an image", creditCost: 1 }),
	],
	creditPacks: [
		creditPack({ id: "credits_300", name: "300 credits", creditAmount: 300, priceCents: 1000 }),
	],
});
```

* The fields are the API's, as on [Catalog](https://docs.integrateledger.com/concepts/catalog.md). `defineCatalog`, `plan`, `feature` and `creditPack` only add types.
* The file holds the **whole** catalog. Items you remove from it are archived when you push.
* There's no `environment`: the API key chooses it, so one file serves Sandbox and Live.
* `ledger.catalog.json` holds the same catalog in the [`POST /v1/catalog`](https://docs.integrateledger.com/api-reference/catalog/publish) body's shape, where packs are `addOns`. It's what the onboarding skill writes, and the default for projects without a `package.json`. If it names an `environment`, the key must match it.

## What a push changes

| Change in the file | What happens |
| - | - |
| A plan's name, credits, price or overage | A new [plan version](https://docs.integrateledger.com/configure-pricing/plans.md#change-a-plan). New subscribers get it; current subscribers keep theirs. A paid plan gets a new Stripe Price, and a new Product when renamed |
| A plan removed | Archived: it can't be attached; current subscribers keep it |
| A feature or credit pack changed | Changed in place. A pack's credits or price can't change while a Checkout for it is open (`purchase_pending`) |
| A feature or credit pack removed | Archived |

Pushing an unchanged file changes nothing and retries any Stripe sync that hasn't finished. In CI, run `ledger diff --exit-code` on pull requests and `ledger push --live --yes` after merge, as in [Deploy](https://docs.integrateledger.com/getting-started/deploy.md).

## Commands

| Command | Does |
| - | - |
| `ledger init [--format ts\|json] [--template] [--force]` | Writes a config from the workspace's catalog, or a starter template when the workspace is empty, there's no key yet, or `--template` is passed. Won't replace a config without `--force`. Also writes the agent skills, like `ledger skills` |
| `ledger pull [--format ts\|json] [--force]` | Writes the workspace's catalog into the config. Leaves a file that already matches untouched, keeps the file's order, and refuses to overwrite changes you haven't pushed unless you pass `--force` |
| `ledger diff [--exit-code]` | Validates the config and shows what `push` would change. `ledger plan` is the same command |
| `ledger push [--yes]` | Validates, diffs, and publishes the whole catalog with `POST /v1/catalog`. Live asks for confirmation; `--yes` skips it |
| `ledger migrate stripe [--dry-run]` | Moves the connected Stripe account's subscribers onto Ledger: proposes a plan per monthly Price, reports what it imports and refuses, then publishes the plans and imports each customer. Resumable. See [Migrate from Stripe Billing](https://docs.integrateledger.com/migrate-from-stripe.md) |
| `ledger skills` | Writes the `ledger-catalog` and `ledger-integrate` [agent skills](https://docs.integrateledger.com/ai.md#ledger-skills) for this CLI version into `.agents/skills/`, replacing older copies. Needs no key |
| `ledger validate` | Checks the config with the API's rules. Needs no key or network |
| `ledger login [--live]` | Reads a key (hidden prompt, or stdin when piped), checks it, and saves it to `.ledger/credentials.json` with mode 600, adding `.ledger/` to `.gitignore` |
| `ledger logout [--live]` | Removes that environment's saved key |
| `ledger whoami` | Shows the environment, workspace, API URL, where the key came from, and the catalog's size |

## Flags

| Flag | Meaning |
| - | - |
| `--live` | Act on Live. The default is Sandbox |
| `-c`, `--config <path>` | The config file. By default the CLI looks here for `ledger.config.ts`, `.mts`, `.js`, `.mjs`, then `ledger.catalog.json`, and stops if it finds more than one |
| `--json` | Print one JSON object on stdout and never prompt |
| `--api-url <url>` | The Ledger API. Default: `LEDGER_API_URL`, else `https://api.integrateledger.com` |
| `-y`, `--yes` | Push to Live without the prompt |
| `--format ts\|json` | For `init` and `pull`: the file to write. Default: the existing file's, else TypeScript in a JavaScript project and JSON elsewhere |
| `--template` | For `init`: the starter catalog, even if the workspace has one |
| `--force` | For `init` and `pull`: overwrite the config |
| `--exit-code` | For `diff`: exit 2 when there are changes |
| `--dry-run` | For `migrate`: report only |
| `--credits <price_…>=<n>` | For `migrate`, repeatable: a new plan's monthly credits, instead of the prompt |
| `--feature <id>=<n>` | For `migrate`, repeatable: a feature and its credit cost, when the config has none |
| `--customer-id <key>` | For `migrate`: take your customer ID from this Stripe customer metadata key. Default: the Stripe customer ID |
| `--no-color` | Plain output. `NO_COLOR` does the same; `FORCE_COLOR` turns colour on outside a terminal |

## API keys

The key's prefix is its environment: `ledger_test_` for Sandbox, `ledger_live_` for Live. The CLI uses the first key for the selected environment from:

1. `LEDGER_API_KEY`
2. `.ledger/credentials.json` (from `ledger login`)
3. `LEDGER_API_KEY` in `.env.local`, then `.env`

A key for the other environment is skipped, so a Live key in your shell never publishes without `--live`. The key needs the `catalog:read` and `catalog:write` actions, and for `migrate` also `customers:read`, `customers:write` and `billing:write`. The CLI never prints it.

## Exit codes

| Code | Meaning |
| - | - |
| `0` | Success |
| `1` | An error: an invalid config, no key, an API error, or a Live push that wasn't confirmed |
| `2` | `ledger diff --exit-code` found changes |

## JSON output

With `--json`, `diff` prints:

```json theme={"system"}
{
  "environment": "sandbox",
  "organizationId": "org_…",
  "path": "ledger.config.ts",
  "hasChanges": true,
  "changes": [
    {
      "kind": "plan",
      "id": "pro",
      "action": "update",
      "name": "Pro",
      "summary": "1,000 credits/month, $39.00/month",
      "fields": [{ "field": "monthlyPriceCents", "from": 2900, "to": 3900 }],
      "effects": [
        { "code": "new_plan_version", "message": "New plan version: …" },
        { "code": "stripe_price", "message": "Stripe: a new $39.00/month Price on the \"Pro\" Product" }
      ]
    }
  ],
  "unsyncedPlans": []
}
```

* `kind` is `plan`, `feature` or `credit_pack`; `action` is `add`, `update` or `archive`.
* Effect codes: `new_plan_version`, `stripe_product_and_price`, `stripe_price`, `stripe_overage_product`, `stripe_none`, `subscribers_keep_plan`, `in_place`, `purchase_pending_risk`, `archived`.
* `push` adds the API's `changed` and `stripeSync` (`synced`, `pending`, `failed`, `awaiting_connection`).
* `pull` prints `{ environment, organizationId, path, counts, wrote }`; `init` prints `{ path, format, source, environment, counts, skills }` with `source` `workspace` or `template`; `skills` prints `{ skills, linkCommand }`, each skill with `{ name, path, status }` and `status` `written`, `updated` or `unchanged`.
* Errors print `{ "error": { "code", "message", "issues"? } }`, where `issues` lists `{ path, message }` with paths like `plans.1.monthlyPriceCents`. Codes include `invalid_config`, `no_api_key`, `confirmation_required`, `local_changes`, `config_not_found`, `unauthorized` and `purchase_pending`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.