# PyAI CLI handbook

Use `pyai` to render voiceovers, transcribe recordings, dub audio, manage Agent
profiles, and inspect the PyAI API from a terminal. The same commands work in
scripts and in coding agents with shell access, including Cursor, Claude Code,
and Codex. `--json` provides structured results; `--help` and `schema` describe
the installed command surface.

**Version:** the expanded CLI is included in `@pyai/sdk` 0.5.0. Install it from
npm with `npm install -g @pyai/sdk@0.5.0`, then run `pyai login` for browser
sign-in or provide `PYAI_API_KEY` for unattended automation.

## Contents

- [Install and verify](#install-and-verify)
- [First five minutes](#first-five-minutes)
- [Shortcuts and one-liners](#shortcuts-and-one-liners)
- [Authentication and profiles](#authentication-and-profiles)
- [Speak and Hear](#speak-and-hear)
- [Dub in one command](#dub-in-one-command)
- [Jobs, Cast, and voice design](#jobs-cast-and-voice-design)
- [Manage resources with JSON](#manage-resources-with-json)
- [Command reference](#command-reference)
- [API discovery and generic requests](#api-discovery-and-generic-requests)
- [Agent and project handoff](#agent-and-project-handoff)
- [Automation contract](#automation-contract)
- [Troubleshooting](#troubleshooting)

## Install and verify

Install the published package (Node.js 22 or newer recommended):

```bash
npm install -g @pyai/sdk@0.5.0
pyai --version
pyai login
pyai speak "Hello from PyAI." -o hello.wav
```

For development, use Node.js 22 or newer to build and test a source checkout:

```bash
cd sdk/typescript
npm ci
npm run build
node dist/cli.js --help
npm install -g .
pyai --help
```

The compiled executable is part of `@pyai/sdk` and supports Node.js 18 or newer.
Source tests require a runtime with TypeScript execution support. A build can
also be packaged and installed on another machine:

```bash
# In sdk/typescript after building
npm pack --ignore-scripts
# On the destination machine, using the actual tarball path
npm install -g /path/to/pyai-sdk-0.5.0.tgz
pyai schema --json
```

Use `node dist/cli.js` instead of `pyai` when avoiding a global install. If a
Python SDK installation or older npm install also provides a `pyai` executable,
that explicit path selects this CLI. `pyai --version` identifies the package
version; `pyai schema --json` describes the commands supported by that
installed version.

## First five minutes

With an existing key provided by your secret manager as `PYAI_API_KEY`:

```bash
pyai whoami -j
pyai voices --language en -j
pyai speak "Your appointment is confirmed." -o confirmation.wav
pyai hear confirmation.wav --text-only
```

For a new isolated sandbox:

```bash
pyai auth sandbox -p sandbox
pyai whoami -p sandbox -j
pyai speak "Hello from PyAI." -p sandbox -o hello.wav
```

Each `auth sandbox` call creates a new organization and saves its key privately.
Reuse the saved profile for subsequent commands. Check the returned scopes and
expiry; sandbox keys do not automatically include every product. In particular,
check for `dub:render` before using Dub. An exported `PYAI_API_KEY` takes
precedence over a profile's key, so unset it when intending to use a saved key.

For browser sign-in, an engineer can instead run:

```bash
pyai login -p work
pyai whoami -p work -j
```

## Shortcuts and one-liners

Short forms use the same validation, API routes, and output behavior as the full
commands. They do not silently choose a different account or skip a job stage.

| Shortcut | Full form |
| --- | --- |
| `pyai login` | `pyai auth login` |
| `pyai logout` | `pyai auth logout` |
| `pyai whoami` | `pyai auth status` |
| `pyai use work` | `pyai profiles use work` |
| `pyai say "Hello"` | `pyai speak --text "Hello"` |
| `pyai hear call.wav` | `pyai transcribe --file call.wav` |
| `pyai voices` | `pyai voices list` |
| `pyai models` | `pyai models list` |
| `pyai profiles` | `pyai profiles list` |

| Short option | Long option | Use |
| --- | --- | --- |
| `-o` | `--out` | Save a response or audio file |
| `-f` | `--file` | Upload a local file |
| `-t` | `--text` | Supply speech text |
| `-p` | `--profile` | Select a profile for this command |
| `-j` | `--json` | Emit machine-readable output |
| `-h` | `--help` | Show help |
| `-v` | `--version` | Show package version |

Options still need to be valid for the selected command. Write short options
separately with a space before their value. Quote speech text and paths with
spaces; `-f` means a file, while overwriting is always the explicit `--force`.

```bash
# A voiceover from a text file
pyai speak --text-file script.txt --format mp3 -o narration.mp3

# A transcript your shell can redirect to a text file
pyai hear meeting.wav --text-only > meeting.txt

# Read a pipe, with no temporary input file
printf '%s\n' 'The build is ready.' | pyai speak --text-file - -o build.wav

# Pass raw audio to another program (ffplay installed separately)
pyai speak "Build complete." --format wav -o - | ffplay -autoexit -nodisp -i -

# Take a catalog snapshot (jq is optional and installed separately)
pyai voices --language en -j | jq '.data'

# Inspect the planned request before creating an Agent
pyai agents create --data @agent.json --dry-run -j

# Find more runnable patterns without making an API request
pyai recipes
pyai recipes speak
pyai recipes --json
```

Available recipes are `auth`, `speak`, `transcribe`, `dub`, `agent`, `inspect`,
and `ci`. Recipe examples can make API calls when you execute them; displaying a recipe
is offline. Shell redirection such as `> meeting.txt` is controlled by your
shell and may overwrite that file. The CLI's no-overwrite protection applies
to files written through `--out`.

## Authentication and profiles

### Browser sign-in for engineers

```bash
pyai login
pyai login -p production
pyai whoami -p production -j
```

The CLI opens the PyAI console and displays a short code. Sign in using the
console's email or Google login, match the code, choose an active project, and
approve access. The CLI saves a 30-day API key in the selected local profile.
The secret is never placed in a browser URL or printed in the login receipt.
Approval requires the same owner/admin role as creating a key in the console.
The consent screen shows scopes and expiry; revoke the key from the console's
API Keys screen when access is no longer needed.

For SSH, containers, or a terminal without a browser:

```bash
pyai login --no-browser -p remote
```

Open the printed link on a machine with a browser and approve the matching
code there. The CLI uses outbound polling, so no callback port or port
forwarding is needed. Requests expire after ten minutes. `--login-timeout
SECONDS` can shorten the local wait. A decline, expired code, or lost exchange
returns an error and preserves the existing profile. Begin a new login after a
failed exchange; an exchanged grant cannot be replayed.

`login` uses the web flow even when `PYAI_API_KEY` is exported. `--web` selects
that flow explicitly. In CI the link is printed without launching a browser
unless `--web` is supplied. Unattended jobs should use an environment key.
An older deployment returns `browser_login_unavailable`; key authentication
continues to work.

### Existing keys and unattended agents

A key in `PYAI_API_KEY` works directly without saving a profile. Supply it from
your CI secret store or secret manager. To persist an existing key locally,
use stdin so its value is not part of the command's arguments:

```bash
printf '%s\n' "$PYAI_API_KEY" | pyai login --key-stdin -p production
pyai profiles -j
```

`--api-key KEY` is also accepted when explicitly needed. It may be visible in
shell history or process arguments; environment variables or stdin avoid that
argument exposure. Do not combine browser flags with key-input flags.

### Account selection and storage

```bash
pyai use production
pyai agents list -p sandbox -j
pyai logout -p sandbox
```

Login and sandbox creation select the saved profile as active. `use` changes
the default. `--profile NAME` selects a profile for one command; `PYAI_PROFILE`
is its environment equivalent. `profiles list` is local; `whoami` checks the
selected key against `GET /v1/me` and reports its organization and scopes.
Logout removes a local profile; it does not revoke the server-side key.

| Setting | Precedence, highest first | Default |
| --- | --- | --- |
| API key | `--api-key`, `PYAI_API_KEY`, selected profile | None |
| Base URL | `--base-url`, `PYAI_BASE_URL`, selected profile | `https://api.pyai.com` |
| Profile | `--profile`, `PYAI_PROFILE`, saved active profile | `default` |
| Config directory | `PYAI_CONFIG_DIR`, `$XDG_CONFIG_HOME/pyai` | `~/.config/pyai` |

`https://api.pyai.com/v1` is accepted as a base URL too. HTTPS is required,
except HTTP on loopback for local development. Profiles are plaintext in
`config.json`, with directory mode `0700` and file mode `0600` where POSIX
permissions are supported. Treat the file as a credential store, exclude it
from source control, and use environment authentication on shared runners.

## Speak and Hear

### Speech output

```bash
pyai speak "Hello from PyAI." -o hello.wav -j
pyai voices --language en -j
pyai speak -t "Welcome back." --voice VOICE_ID --format mp3 -o welcome.mp3
pyai speak --text-file script.txt --format g711_ulaw -o prompt.raw
```

Replace `VOICE_ID` with an identifier returned by the catalog. Supply exactly
one text source: a positional string, `--text`, or `--text-file`. `--text-file
-` reads UTF-8 stdin. With no explicit text input, piped stdin is read
automatically: `echo "Hello" | pyai speak -o hello.wav`. With an interactive
terminal and no input, the command returns an input error promptly. `--model` defaults to `pyai-speak`; formats include `wav`,
`mp3`, `opus`, `aac`, `flac`, `pcm`, `g711_ulaw`, and `g711_alaw`. Optional
`--sample-rate` accepts 8000, 16000, 24000, or 48000; G.711 requires 8000.

Speak defaults to `pyai-speak.wav`, or a matching extension for `--format`
(`.raw` for G.711). Choose a new name for each artifact or intentionally pass
`--force`. The output directory must exist. File writes use a temporary file
and complete atomically; a failed transfer does not leave a partial destination.
`--out -` streams bytes to stdout and cannot be combined with `--json`.
Binary output directly to an interactive terminal is refused.

A file-producing command with `--json` returns a receipt containing `path`,
`bytes`, and `content_type`. It does not put base64 audio in JSON. The response
format determines the bytes; changing only a filename extension does not
transcode audio.

### Local and URL transcription

```bash
pyai transcribe meeting.wav -j
pyai hear meeting.wav --text-only > meeting.txt
cat meeting.wav | pyai hear -f - --filename meeting.wav -j
pyai transcribe --url https://example.com/meeting.wav --wait -j
pyai transcribe --url https://example.com/meeting.wav --wait --text-only
```

A positional input is a local file, or a hosted URL when it begins with
`http://` or `https://`. Use `--file` or `--url` to select the input type
explicitly.
Local transcription calls Hear synchronously. URL transcription creates an
asynchronous job; `--wait` waits for completion, and `--poll` is a compatibility
alias. `--diarize` and `--idempotency-key` apply to URL jobs. `--language` is an
STT hint for a local file, but only a Recap summarization hint for URL jobs.
Read the live API contract for current limits and language support.

`--text-only` emits the transcript text with a trailing newline and requires
`--wait` for a URL job. It is incompatible with `--json`; use normal JSON when
you need segments, speaker information, timestamps, or the job envelope.
Large asynchronous results may be returned through `result_url` rather than
inline text. Retain the job's JSON when downstream processing needs its full
result; do not assume every successful job has an inline `text` field.

## Dub in one command

Discover the deployment's current input and output language capabilities first:

```bash
pyai request GET /healthz/dub -j
```

When the deployment lists the desired language pair and your key includes
`dub:render`, submit, wait, and download audio in one command:

```bash
pyai dub interview.wav --from en --to hi -o interview-hi.wav --wait-timeout 600
```

The shortcut accepts a local source or a positional HTTP(S) audio URL. `--from` optionally sets the source language
and `--to` sets the required target. Output defaults to `pyai-dub.wav`;
use `--out` to choose another path. Check language capabilities
before selecting values; languages are enabled per deployment. The command
performs one submission, bounded polling, then an audio download. It does not
play audio or start a realtime session. The final JSON form is a file receipt with `job_id`, `status`, `path`,
`bytes`, and `content_type`.

For independent lifecycle control, video inputs, or extra API fields:

```bash
pyai dub create -f interview.mp4 --language hi --source-language en -j
pyai dub create --url https://example.com/interview.wav --language hi -j
pyai dub create -f interview.mp4 --language hi \
  --data '{"preserve_background":true,"timing":"source"}' -j
pyai dub get DUB_JOB_ID -j
pyai dub wait DUB_JOB_ID --wait-timeout 600 -j
pyai dub audio DUB_JOB_ID -o dubbed.wav -j
pyai dub video DUB_JOB_ID -o dubbed.mp4 -j
```

Replace `DUB_JOB_ID` with the returned `job_id`. `dub create` accepts exactly
one of `--file` or `--url`. `--file -` reads binary stdin and `--filename`
supplies its upload filename. Additional `--data` must be an object and cannot
repeat a field set by flags. Binary input and JSON `--data @-` cannot both
consume stdin. Video download requires a job with a video source.

A polling timeout does not cancel the remote job. Use the job path returned
in the error to inspect or resume waiting, then download with `dub audio`.
Do not blindly resubmit after a timeout or an ambiguous network failure.

## Jobs, Cast, and voice design

Separate submit/wait/download commands are useful for long jobs, CI stages,
and resuming work after a terminal disconnect:

```bash
pyai jobs list --limit 20 -j
pyai jobs get JOB_ID -j
pyai jobs wait JOB_ID --wait-timeout 300 -j
pyai jobs cancel JOB_ID -j
pyai cast capabilities -j
pyai cast direct --data '{"text":"Welcome. Let us begin."}' -j
pyai cast render --data @cast-render.json -j
pyai cast wait RENDER_ID --wait-timeout 300 -j
pyai cast audio RENDER_ID -o performance.wav -j
pyai clones create -f reference.wav --name "Support voice" -j
pyai design create --data '{"prompt":"A warm, calm English narrator"}' -j
pyai design wait DESIGN_ID --wait-timeout 300 -j
pyai design save DESIGN_ID --data '{"candidate_id":"c1","name":"Narrator"}' -j
```

Use IDs from actual API responses, including the candidate ID returned by
voice design. Cast render bodies contain `voice` and either `script` or
`lines`. Directed lines contain `text`, `emotion`, and `intensity`; discover
supported values through `cast capabilities`. `cast speech` produces audio
from one JSON request and requires `--out`.

| Job family | Successful state | Failed terminal states |
| --- | --- | --- |
| Transcription | `completed` | `failed`, `cancelled` |
| Voice design | `completed` | `failed` |
| Cast | `done` | `error` |
| Dub | `done` | `error` |

Waiting commands use the appropriate status vocabulary and return a nonzero
exit code for a failed job. `jobs cancel` stops pending work and retains
already completed results. Explicit submission and wait commands return API
JSON; only the top-level Dub shortcut also downloads the audio automatically.

## Manage resources with JSON

Keep configuration in files that engineers and coding agents can review:

```bash
pyai agents create --data '{"name":"Support","greeting":"How can I help?"}' -j
pyai agents update AGENT_ID --data @agent-update.json --dry-run -j
pyai agents update AGENT_ID --data @agent-update.json -j
printf '%s\n' '{"greeting":null}' | pyai agents update AGENT_ID --data @- -j
pyai vocabulary set --data '{"terms":["AcmeCloud"],"enabled_for":["batch"]}' -j
```

`--data` accepts inline JSON, `@PATH`, or `@-` for stdin. Dedicated mutations
expect an object. `request` also accepts arrays, scalars, and null where the
API supports them. Agent updates modify only supplied fields; use `null` to
clear a field. Consult the API schema before constructing nested tool bindings,
metadata, extraction schemas, or other product configuration.

Commands do not open terminal confirmation prompts, including explicit delete
and cancel commands. Use `--dry-run` to review intent before a mutation. A dry
run validates and reads local input, but does not make the planned API call;
it does not validate remote permissions, available credit, or every server-side
schema rule. Request data may itself be sensitive, so handle saved previews
and configuration files appropriately.

Paginated list commands support `--limit 1..100` and `--cursor TOKEN`. Copy the
returned `next_cursor` into the next request; one command returns one page.
Use `request --query name=value` for API filters without a dedicated option.

## Command reference

Each verb after a group retains that group: `agents get ID`, `agents create`,
and so on. Run `pyai GROUP --help` or `pyai COMMAND --help` for exact flags.
Use `pyai help all` for the expanded help listing.
The offline schema is authoritative for the installed build.

| Group | Commands |
| --- | --- |
| Identity | `auth login`, `auth sandbox`, `auth status`, `auth logout` |
| Profiles | `profiles list`, `profiles use NAME` |
| Catalogs | `models list`, `voices list`, `voices get ID` |
| Speech | `speak [TEXT]`, `transcribe [FILE_OR_URL]` |
| Shortcuts | `login`, `logout`, `whoami`, `use NAME`, `say [TEXT]`, `hear [FILE]`, `dub INPUT --from LANG --to LANG --out PATH` |
| Agents | `agents list`, `get ID`, `create`, `update ID`, `delete ID` |
| Transcription jobs | `jobs list`, `create`, `get ID`, `wait ID`, `cancel ID` |
| Cloned voices | `clones list`, `create`, `delete ID` |
| Voice design | `design create`, `get ID`, `wait ID`, `save ID` |
| Cast | `cast capabilities`, `direct`, `speech`, `render`, `get ID`, `wait ID`, `audio ID` |
| Dub jobs | `dub create`, `get ID`, `wait ID`, `audio ID`, `video ID` |
| Recap | `recap list`, `get ID`, `config`, `configure` |
| Trace | `trace list`, `get ID`, `config`, `configure`, `findings`, `violations`, `exposure` |
| Tools | `tools list`, `get ID`, `create`, `update ID`, `delete ID` |
| Hear vocabulary | `vocabulary get`, `set` |
| AMD | `amd calls`, `get ID`, `config`, `configure` |
| Discovery | `schema`, `schema --openapi`, `recipes [NAME]`, `help` |
| Project setup | `init DIRECTORY --template agent\|typescript\|python` |
| Generic API | `request METHOD /PATH` |
| Diagnostics | `doctor`, `smoke` |

JSON mutations require `--data`; binary downloads and `cast speech` require
`--out`. Voice enrollment and Dub submission use their file/URL flags.

## API discovery and generic requests

```bash
pyai schema -j
pyai schema speak -j
pyai schema agents create -j
pyai schema --openapi -j > openapi.json
pyai request GET /v1/voices --query language=en --query tier=natural -j
pyai request POST /v1/agents --data @agent.json --dry-run -j
pyai request GET /v1/cast/render_jobs/RENDER_ID/audio -o render.wav
```

`schema` is offline and describes CLI syntax. Supply a command or group to
reduce context: `schema agents` returns the Agent commands, while
`schema agents create` returns that command. Aliases such as `say` resolve
to their canonical commands. `schema --openapi` fetches the
configured deployment's current API contract without requiring authentication.
The API contract describes bodies, responses, scopes, and endpoint availability;
the CLI schema does not replace it. `--openapi` does not accept a command
filter. Use `request` for routes without dedicated
commands and newly added API features.

Generic requests accept `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, and `DELETE`.
Paths must be relative to the configured API origin, such as `/v1/models`.
Absolute URLs, cross-origin requests, redirects, and credential query parameters
are rejected. Repeat `--query` to add parameters. `GET` and `HEAD` do not accept
`--data`. `request` handles JSON and raw response downloads; specialized upload
commands handle multipart audio.

Terminal JSON redacts credential fields. If an API response includes a token
your application needs, `request ... --out response.json` saves the original
response with private file permissions and emits only a path/size receipt.
That file can contain secrets; it should not be committed or printed to logs.

## Agent and project handoff

Create a starter directory without credentials or network calls:

```bash
pyai init voice-project
pyai init voice-ts --template typescript
pyai init voice-python --template python
pyai init voice-project --template agent --dry-run -j
```

`agent` is the default template. The target must be a new directory with an
existing parent; scaffolding
does not merge into or overwrite an existing project. Every template includes `README.md`, `PYAI.md`, `.env.example`, and
`.gitignore`. The agent template adds `agent.json`, `speech.json`, and
`job.json`; TypeScript adds `main.ts` and `package.json`; Python adds `main.py`.
`PYAI.md` provides an integration handoff for an engineer or coding agent. Follow the
starter's README for its files, runtime prerequisites, and next commands.
Scaffolding does not install dependencies, create an account, or make paid API
calls. JSON output includes `directory`, `template`, `files`, `created`, and
`next_steps`. Read the generated files before running the starter.

For an existing repository, point the agent at these resources:

- [CLI agent guide](https://pyai.com/cli-agent-guide.md): compact integration workflow and machine contract.
- [Raw CLI guide](https://pyai.com/cli.md): readable without rendering the website.
- [Canonical CLI documentation](https://docs.pyai.com/guides/cli): task-oriented guide.
- [CLI schema snapshot](https://pyai.com/cli-schema.json): machine-readable CLI syntax for this release; inspect your installed CLI for its supported commands.
- [Live OpenAPI](https://api.pyai.com/openapi.json): API source of truth.
- [API agent index](https://api.pyai.com/llms.txt): product and protocol references.

A useful task instruction is:

> Read PYAI.md, inspect `pyai schema --json`, and fetch `pyai schema --openapi
> --json`. Use PYAI_API_KEY from the environment without printing it. Discover
> voices before selecting one. Preview resource mutations with `--dry-run`.
> Implement the requested workflow with bounded waits, stable error-code
> handling, and a runnable verification command. Use the SDK for realtime audio.

Keep real credentials out of prompts and repository instruction files. The
CLI can configure Agent resources, but that is separate from opening an Omni
conversation. Use the [TypeScript/Python SDK guides](https://docs.pyai.com/guides/sdks)
for realtime transports. The [PyAI MCP server](https://docs.pyai.com/guides/use-pyai-in-cursor) offers a tool
interface for hosts that prefer MCP over shell commands.

## Automation contract

| Global flag | Meaning | Default |
| --- | --- | --- |
| `--json`, `-j` | Compact JSON result; structured errors | Off |
| `--profile`, `-p` | Profile for this invocation | Selected profile |
| `--base-url` | Deployment origin | Profile/env or production API |
| `--api-key` | Explicit opaque key | Environment/profile |
| `--timeout` | Seconds per HTTP request, including body download | `30` |
| `--retries` | Read retry count, integer `0..5` | `2` |
| `--dry-run` | Preview the operation without executing it | Off |

Waiting commands additionally accept `--wait-timeout` (default 120 seconds)
and `--poll-interval` (default 2 seconds). The wait deadline bounds polling;
individual requests also respect `--timeout`. Browser login has its separate
`--login-timeout`. A local timeout does not revoke credentials or cancel jobs.

Automatic retries apply only to reads, with rate-limit delays respected.
Mutations are not automatically retried to avoid duplicate resources or jobs.
Commands accepting JSON mutation bodies also accept `--idempotency-key TOKEN`,
with support determined by the API route. Reuse an idempotency key only for
retries of the same intended operation and body. Never assume the option makes
every endpoint idempotent.

With `--json`, stdout contains one command result and errors use stderr:

```json
{"error":{"code":"unauthorized","message":"...","status":401}}
```

`status`, `request_id`, job `path`, and other details are present when available.
Branch on `error.code` and the exit code; do not parse human wording. Unknown
fields may be added, and normal API result shapes follow the live API contract.
Do not assume all successful commands wrap output under a `data` property.

Browser login is the stderr exception to a single final error object: it emits
newline-delimited public events such as `authorization_required` with a link,
user code, and expiry, and `browser_unavailable` if launch fails. Its final
success receipt is still a single stdout JSON object. Progress never mixes
with stdout. Diagnostics emit one result object with `--json`, including failed
checks, and set a nonzero status when a check fails.

| Exit code | Meaning | Typical next step |
| --- | --- | --- |
| `0` | Success | Consume result/artifact |
| `1` | API, job, or operational failure | Inspect error code and job status |
| `2` | Invalid arguments or local configuration/input | Fix flags, JSON, paths, or config |
| `3` | Authentication or permission failure | Check key, profile, and scopes |
| `4` | Network failure or timeout | Inspect remote state before a mutation retry |
| `130` | Interrupted | Resume an existing job if appropriate |

The CLI bounds stdin at 128 MiB; use a file or hosted URL for larger inputs.
This bound is a client limit, not a promise about server upload limits.
Secrets known to the CLI and credential-shaped JSON fields are redacted from
terminal output. Redaction does not replace reviewing arbitrary application
payloads, downloaded responses, or shell tracing settings.

## Troubleshooting

| Symptom | Action |
| --- | --- |
| New command is unknown | Inspect `pyai schema -j`; build this checkout and check which `pyai` executable your shell resolves. The npm release may be older. |
| Browser login is unavailable | Deploy the matching browser-auth components or use `PYAI_API_KEY` / `login --key-stdin`. |
| Browser does not open on SSH | Run `login --no-browser` and approve the printed code from another device. |
| Login expired or was declined | Run login again and approve the new code before expiry. Existing profile credentials remain unchanged on failure. |
| Wrong account despite `-p` | An exported `PYAI_API_KEY` overrides the profile key; also inspect `PYAI_BASE_URL` and `PYAI_PROFILE`. |
| `401` / exit `3` | Check expiry and revocation, then login or replace the key. |
| `403` / exit `3` | Check required scope and organization/project access. Repeating the request will not add permissions. |
| `402` | Check organization credit, plan limits, and per-key budget in the console. |
| `429` | Honor retry delays and check concurrency/daily caps. Avoid a tight retry loop. |
| Output already exists | Pick another name or intentionally pass `--force`; ensure the parent directory exists. |
| `--out -` rejected | Pipe bytes to a process and remove `--json`, or use a file destination for a JSON receipt. |
| Job timed out | Inspect the returned job path and use `jobs wait`, `cast wait`, `design wait`, or `dub wait`; the job may still be running. |
| Text-only job has no inline transcript | Inspect normal job JSON and its `result_url`; preserve the result envelope for larger outputs. |
| JSON sent to an API is rejected | Fetch the live OpenAPI; CLI `--dry-run` does not prove server schema validity. |

`pyai doctor` checks credentials, catalogs, and a Speak-to-Hear round trip.
`pyai smoke` runs a smaller catalog and synthesis check. These diagnostics make
real API calls and consume the applicable usage quota; use `--dry-run` to
inspect the request plan first. For a problem report, retain the CLI schema,
exit/error code, request ID, and a redacted reproduction. Never include the key.

Realtime microphone sessions, live Hear streams, Omni conversations, and live
AMD streams require a realtime client. Use the SDK and canonical protocol docs
for those workflows; the CLI covers files, REST operations, and job lifecycles.
