# kusty CLI

> kusty is a command-line interface for the Kustomer API built for AI agents and scripts. Every command returns a stable JSON envelope and a numbered exit code. Install it, connect an org with an API key or OAuth, and make your first calls.

Source: https://developer.kustomer.com/docs/ai/kusty

**kusty** is the Kustomer API as a command-line tool. It's designed to be driven by models as much as by people: output is JSON by default, every error has a stable code and a hint, exit codes tell you what went wrong without parsing anything, and nothing ever stops to wait for input. The same binary also runs as an [MCP server](https://developer.kustomer.com/docs/ai/kusty-mcp), so Claude Code, Cursor, Codex and Gemini CLI can call every Kustomer endpoint as a tool.

_A terminal running kusty. 'kusty api GET /v1/customers/email=sam\@example.com' prints a one-line JSON envelope with schema_version, data and meta; piping it to jq prints the customer's name. With a missing API key, the error envelope has code AUTH_REQUIRED and a hint, and echo $? prints 3. A --dry-run POST echoes the resolved request with the Authorization header masked; a DELETE without --confirm --yes fails with DESTRUCTIVE_CONFIRMATION_REQUIRED. kusty mcp install registers kusty with Cursor and Claude Code._

|        |                                                                                                                                                             |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Covers | Every endpoint in the [public API reference](https://developer.kustomer.com/api): 104 resource commands with 411 subcommands, plus `kusty api` for any path |
| Output | One JSON envelope per command (`jsonl` by default), or `json`, `yaml`, `table`                                                                              |
| Auth   | An API key, or OAuth so each person signs in as themselves                                                                                                  |
| Agents | `kusty mcp serve` exposes the same commands as MCP tools                                                                                                    |

## Install

kusty is a single Go binary. To build it from source you need Go 1.26 and `make`. From a checkout of the kusty source, build the public profile and install it to `~/.local/bin`:

```bash
make install BUILD_PROFILE=public
```

Make sure `~/.local/bin` is on your `PATH`, then check the install:

```bash
kusty version
```

Turn on shell completion with `kusty completions zsh` (or `bash`, `fish`, `powershell`), for example `kusty completions zsh > "${fpath[1]}/_kusty"`.

## Connect an org

kusty targets orgs through **profiles**. A profile names an org and says how to authenticate to it. You can set one up interactively with `kusty config`, or with flags.

**API key**

The quickest way to start. Create an API key in **Settings › Security › API Keys** with the roles you need, then:

```bash
kusty profile add prod --org-subdomain acme --default
export KUSTY_API_KEY=eyJhbGciOi...
kusty auth whoami
```

`--org-subdomain acme` points at `https://acme.api.kustomerapp.com`. You can pass your dashboard URL instead with `--tenant-url https://acme.kustomerapp.com`.

**OAuth (each user signs in)**

With OAuth, everyone authenticates as themselves, with their own permissions, instead of sharing a key. An admin sets it up once per org, then each user logs in.

**Admin, once:** with an API key that has the `org.admin.apps` role, register kusty's private app and mint its client secret:

```bash
export KUSTY_API_KEY=eyJhbGciOi...   # key with org.admin.apps
kusty app register                   # registers and installs the private "Kusty CLI" app
kusty app secret                     # prints the OAuth client ID and secret
kusty app status                     # confirms it's installed
```

Share the tenant URL, client ID, client secret and scopes with your team. `kusty config` shows them as a copy-paste bundle.

**Each user:** run `kusty config`, create a profile with your tenant URL and the OAuth credentials, then:

```bash
kusty auth login     # opens your browser to sign in
kusty auth whoami
```

`kusty config` is a terminal UI that walks through the same steps. It's the only interactive part of kusty; scripts and agents use the `profile`, `app` and `auth` commands instead.

### Sessions and refresh

`kusty auth login` runs the OAuth authorization-code flow through a local callback on port 53682, 53683 or 53684. The resulting tokens are stored per profile in a file only you can read, in `sessions/` next to the config file.

Access tokens last 24 hours. kusty renews them from the refresh token automatically, including inside a long-running MCP server, so you shouldn't need to log in again for weeks.

> **Refresh needs the client secret:** Kustomer accepts PKCE for the initial login but requires client credentials to refresh. A profile with only a client ID can log in but can't refresh, so its session ends after 24 hours, and `kusty auth refresh` reports `invalid_client: cannot retrieve client credentials`. Add the client secret to the profile and log in again.

| Command              | Does                                                                              |
| -------------------- | --------------------------------------------------------------------------------- |
| `kusty auth status`  | Shows token expiry and whether the session can refresh, without changing anything |
| `kusty auth refresh` | Renews the access token now, and shows the real error if it can't                 |
| `kusty auth logout`  | Deletes the stored session                                                        |

By default kusty requests the scopes `org.permission.customer`, `org.permission.conversation`, `org.permission.message`, `org.permission.user` and `offline_access` (needed for refresh). Set `oauth_scopes` on a profile to request others.

### Profiles and the config file

Profiles live in `~/.config/kusty/config.toml`, or wherever `KUSTY_CONFIG` points.

```toml title="~/.config/kusty/config.toml"
default_profile = "prod"

[profiles.prod]
org_subdomain = "acme"

[profiles.sandbox]
org_subdomain = "acme-sandbox"
api_key_env = "SANDBOX_KUSTY_API_KEY"
```

- `api_key_env` makes a profile read its key from a different variable, so you can keep a key per org in your shell profile.
- Settings resolve in this order: command-line flag, then `KUSTY_*` environment variable, then profile, then built-in default.
- For credentials, an API key wins over an OAuth session. An admin profile that holds the OAuth client credentials for sharing still uses its own key.

```bash
kusty profile use sandbox                      # change the default
kusty --profile sandbox api GET /v1/users/current   # one command
KUSTY_PROFILE=sandbox kusty auth whoami        # one shell
kusty context show                             # which profile, org and credential would be used, offline
```

`kusty context show` doesn't touch the network: it reports the profile, org, base URL, `credential_source` (`env:KUSTY_API_KEY`, `oauth`, `flag`, …) and whether a credential is present. Put it at the top of scripts. `kusty auth whoami` proves the credential works by calling `/v1/users/current`.

## Make requests

There are two ways to call the API.

**Named commands** cover every operation in the [API reference](https://developer.kustomer.com/api), one per operation, with path and query parameters as flags:

```bash
kusty customers get-by-email --email sam@example.com
kusty conversations list --pageSize 10 --output table
kusty evaluations runevaluation --evaluationId 6716c1d2c4e5f60012ab36ea
```

Run `kusty <resource> --help` to list a resource's subcommands, or see the [command reference](https://developer.kustomer.com/docs/ai/kusty-reference#resource-commands).

**`kusty api`** sends any request to any path, which is handy for endpoints you already know from the [API reference](https://developer.kustomer.com/api):

```bash
kusty api GET /v1/customers --limit 5
kusty api PATCH /v1/customers/6716a3a9c4e5f60012ab34c1 --field name="Sam Rivera"
kusty api POST /v1/customers --data @customer.json
echo '{"name":"Sam Rivera"}' | kusty api POST /v1/customers --data @-
kusty api GET /v1/customers --all | jq -r '.data.attributes.name'
```

| Flag                                             | Builds                                                                                                 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| `--data '{…}'`, `--data @file.json`, `--data @-` | The body, inline, from a file, or from stdin                                                           |
| `--field a.b=text`                               | The body one field at a time, as strings. `a.b:=42` sets raw JSON: numbers, booleans, arrays, objects. |
| `--query k=v`                                    | Extra query parameters. Repeatable.                                                                    |
| `--header 'Name: value'`                         | Extra headers. Repeatable.                                                                             |
| `--all`                                          | Follows `links.next` and streams every page, one item per line                                         |
| `--limit N`, `--cursor C`                        | `pageSize` and `page`                                                                                  |

`--data` wins if you pass both `--data` and `--field`. Field paths use dots, with `emails.0.email` for array items and `-1` to append (`tags.-1=vip`).

## The output contract

Every command writes exactly one thing to stdout: a success envelope, a stream of JSON lines, or an error envelope.

```json title="Success"
{
  "schema_version": "1",
  "data": { "type": "customer", "id": "6716a3a9c4e5f60012ab34c1", "attributes": { "name": "Sam Rivera" } },
  "meta": { "request_id": "9f2c41d8…", "duration_ms": 142, "next_cursor": "…" },
  "warnings": []
}
```

```json title="Error"
{
  "schema_version": "1",
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "no API key configured",
    "hint": "set SANDBOX_KUSTY_API_KEY or pass --api-key",
    "retryable": false
  }
}
```

- `data` is the JSON:API `data` from Kustomer's response, so `.data.attributes.name` works the same as with the raw API.
- The default format is `jsonl`, one envelope per line, whether you're in a terminal, a pipe, or an agent. Use `--output json` for pretty JSON, `yaml`, or `table` to read results yourself.
- `--fields id,attributes.name` keeps only the paths you list, which saves tokens when an agent needs one value.
- Error envelopes come with a non-zero [exit code](https://developer.kustomer.com/docs/ai/kusty-reference#exit-codes). The `hint` names the specific fix, such as the environment variable the active profile expects.
- Human-readable diagnostics go to stderr; structured logs go to a JSON-lines file at `$XDG_STATE_HOME/kusty/kusty.log`. Every request's `request_id` appears in both the log and `meta`.

## Safety rails

| Rail              | Behavior                                                                                                                                                                                                                                                    |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dry-run`       | Prints the resolved request (method, base URL, path, query, headers and body) instead of sending it. The `Authorization` header is masked.                                                                                                                  |
| `--confirm --yes` | Required together for every `DELETE`. Without both, kusty refuses with `DESTRUCTIVE_CONFIRMATION_REQUIRED`.                                                                                                                                                 |
| `--read-only`     | Rejects every write with `READ_ONLY`. Set `KUSTY_READ_ONLY=1` to make it the default for a shell or an agent.                                                                                                                                               |
| Validation        | Request bodies are checked against the API's JSON Schemas before sending, so typos and bad enum values fail fast with `SCHEMA_VALIDATION_FAILED`.                                                                                                           |
| Retries           | Up to three retries, honoring `Retry-After` on 429. A 5xx is retried only when repeating is safe: `GET`, `HEAD`, `PUT`, `DELETE`, or a request with `--idempotency-key`. A failed `POST` or `PATCH` isn't resent, because it may already have taken effect. |

## Release notes

`kusty whats-new` prints kusty's release notes, newest first, with no login needed. Narrow them with a topic, `--product cli|mcp`, `--since 2026-10-01` or `--limit 5`.

- [Use kusty from AI tools](https://developer.kustomer.com/docs/ai/kusty-mcp): Register the MCP server with Claude Code, Cursor, Codex or Gemini CLI.
- [Command reference](https://developer.kustomer.com/docs/ai/kusty-reference): Every global flag, error code, exit code and resource command.
