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

# Command Reference

> Every `slideless` command, with flags, examples, and expected output.

## Global flags

These flags are accepted by every command that talks to the backend:

| Flag               | Purpose                                                |
| ------------------ | ------------------------------------------------------ |
| `--api-key <key>`  | Override the active profile's API key                  |
| `--api-url <url>`  | Override the backend base URL                          |
| `--profile <name>` | Use a specific saved profile instead of the active one |
| `--json`           | Output a stable JSON shape for machine consumption     |

## Auth (OTP)

Terminal-only signup and login. No browser, no dashboard. See [`cli/auth`](/cli/auth) for the full flag reference and error-code table.

### `slideless auth signup-request`

Email a 6-digit signup code to a brand-new address. Refuses (`USER_ALREADY_HAS_ORGANIZATION`) if the email already owns an organization.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless auth signup-request --email you@example.com
slideless auth signup-request --email you@example.com --json
```

### `slideless auth signup-complete`

Verify the code; create user, organization, and a `cko_` key; save it as the active profile. `--first-name` is required (stored as display name + used in onboarding emails); `--last-name` is optional. Without `--company`, the org defaults to `"<first-name>'s workspace"`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Minimal
slideless auth signup-complete --email you@example.com --code 123456 --first-name "Alex"

# Populate user + organization at the same time
slideless auth signup-complete \
  --email you@example.com --code 123456 \
  --first-name "Alex" --last-name "Morgan" \
  --company "Acme" --description "We make widgets" \
  --brand-primary "#0a0a0a" --logo ./logo.png
```

### `slideless auth login-request`

Email a login code to an existing account. Returns `USER_NOT_FOUND` / `USER_HAS_NO_ORGANIZATION` if there's nothing to log in to.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless auth login-request --email you@example.com
```

### `slideless auth login-complete`

Verify the code; mint a fresh `cko_` key for the existing organization; save it as the active profile.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless auth login-complete --email you@example.com --code 123456
slideless auth login-complete --email you@example.com --code 123456 \
  --key-name "ci-runner" --key-expires-in 30
```

## Authentication

### `slideless login`

Save an API key and verify it against the backend. Alias of `slideless config set`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless login                           # interactive prompt (masked input)
slideless login --api-key cko_...         # non-interactive / CI
slideless login --name work-org           # custom profile name
slideless login --skip-verify             # save without calling the backend
```

### `slideless whoami`

Show the currently authenticated identity. Calls `/verifyApiKey` for fresh data; falls back to cached profile data on network error.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless whoami
slideless whoami --json
```

### `slideless verify`

Focused identity check — prints `✓ API key valid` and exits 0 on success, nonzero on failure. Convenient for CI pipelines and troubleshooting.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless verify
slideless verify --json
```

### `slideless use [name]`

List saved profiles or switch the active one.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless use              # list profiles
slideless use work-org     # switch to "work-org"
slideless use --json       # machine-readable profile list
```

### `slideless logout [name]`

Remove a profile. Without an argument, removes the active profile.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless logout
slideless logout personal
```

## Presentations

Slideless v0.5 collapsed the CLI to eight verbs. Each does one thing:

| Verb       | Job                                             |
| ---------- | ----------------------------------------------- |
| `push`     | Upload content (create new, or update existing) |
| `pull`     | Download content to a local folder              |
| `share`    | Mint a public viewer URL                        |
| `unshare`  | Revoke viewer URLs                              |
| `invite`   | Add a dev collaborator                          |
| `uninvite` | Revoke a dev collaborator                       |
| `delete`   | Hard-delete a presentation                      |
| `list`     | Show every deck you can access                  |

The old `share <path>`, `update`, `revoke`, and `token add` commands are gone.

### `slideless push [path]`

Upload content — creates a new deck or updates an existing one based on whether `slideless.json` exists in the target folder.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# New presentation (first push in a folder)
slideless push ./my-deck --title "Q4 Pitch"

# Update (subsequent pushes in the same folder)
slideless push                           # from inside the deck directory
slideless push ./my-deck                 # with explicit path
slideless push ./my-deck --force         # bypass version-conflict check
```

Flags:

| Flag                 | Purpose                                                         |
| -------------------- | --------------------------------------------------------------- |
| `--title <title>`    | Required on first push. Display title shown in the dashboard.   |
| `--entry <filename>` | Folder mode only. Entry HTML file name. Default: `index.html`.  |
| `--force`            | Skip optimistic-concurrency check on updates (bypass conflicts) |
| `--strict`           | Escalate static-scan warnings to hard errors.                   |
| `--message <text>`   | Short commit-style note, logged server-side.                    |

The first successful push writes a **`slideless.json`** at the deck root. That's how the CLI knows the folder is a Slideless deck — don't delete or edit this file. It's added to the built-in ignore list so it never uploads.

**JSON output (new):**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": true,
  "data": {
    "presentationId": "01HXYZ...",
    "version": 1,
    "role": "owner",
    "isNew": true,
    "slidelessJson": "slideless.json",
    "assetsUploaded": 5,
    "assetsDeduped": 0,
    "totalBytes": 42000
  }
}
```

**JSON output (conflict on update):**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": false,
  "status": 409,
  "error": {
    "code": "conflict",
    "message": "Server is at version 5, client expected 3.",
    "nextAction": "Remote has a newer version. Run `slideless pull <id>` to sync, or pass --force."
  }
}
```

A push does **not** mint a viewer URL anymore. Use `slideless share <id>` when you're ready to publish.

### `slideless pull <presentationId> [path]`

Download a presentation to a local folder. Writes `slideless.json` at the root so subsequent `push` commands update this presentation.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless pull 01HXYZ...                 # → ./<title-slug>/
slideless pull 01HXYZ... ./my-deck       # explicit destination
slideless pull 01HXYZ... --at 3          # pin to a specific version
slideless pull 01HXYZ... --force         # wipe and overwrite a non-empty folder
```

Owners and active dev collaborators can pull. Every byte is re-hashed on the client and the download aborts if any SHA-256 mismatches the manifest.

**JSON output:**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": true,
  "data": {
    "presentationId": "01HXYZ...",
    "version": 4,
    "role": "dev",
    "path": "/abs/path/to/deck",
    "filesWritten": 12,
    "bytes": 5420998
  }
}
```

### `slideless share <presentationId>`

Mint a public viewer URL. Owner-only. Replaces the old `token add` command.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless share 01HXYZ...
slideless share 01HXYZ... --name "Acme Corp"
slideless share 01HXYZ... --pin 3                # freeze viewers on v3
```

Flags:

| Flag              | Purpose                                                       |
| ----------------- | ------------------------------------------------------------- |
| `--name <text>`   | Human-readable label for the token. Default: `default`.       |
| `--pin <version>` | Pin the token to a specific version. Default: follows latest. |

### `slideless unshare <presentationId>`

Revoke viewer URLs. The deck itself stays — you can mint new URLs later.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless unshare 01HXYZ...                      # revoke every token
slideless unshare 01HXYZ... --token 01H...       # revoke just one
```

### `slideless invite <presentationId>`

Invite a dev collaborator by email. Owner-only.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless invite 01HXYZ... --email alice@example.com
slideless invite 01HXYZ... --email alice@example.com --message "Happy to have your help!"
```

If Alice already has a Slideless account, she's linked immediately (`status=active`). Otherwise the row is `status=pending` and auto-claims when she signs up. The invite email includes a CLI snippet and a dashboard link.

### `slideless uninvite <presentationId> <collaboratorId>`

Revoke a dev collaborator. Symmetric with `invite`. Revocation is immediate.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless uninvite 01HXYZ... 01H...
```

### `slideless delete <presentationId>`

Hard-delete a presentation. Removes the Firestore doc, every version, every asset, every collaborator row. **Irreversible.**

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless delete 01HXYZ...          # interactive — type the title to confirm
slideless delete 01HXYZ... --yes    # skip prompt (scripts, CI)
```

### `slideless share-email <presentationId>`

Email an existing shared presentation to one or more recipients. Each recipient gets a unique named share link by default — the sender can then see per-recipient opens in the dashboard (or via `slideless get`).

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Single recipient
slideless share-email 01a3b... --to alice@x.com

# Multiple recipients with an optional personal note
slideless share-email 01a3b... \
  --to alice@x.com --to bob@y.com \
  --message "Here's the Q2 deck we discussed." \
  --json

# Reuse an existing token for every recipient (loses per-recipient tracking)
slideless share-email 01a3b... --to alice@x.com --token-id 01a3c... --json
```

Flags:

| Flag               | Notes                                                                                   |
| ------------------ | --------------------------------------------------------------------------------------- |
| `--to <email>`     | Recipient. Repeat for multiple (max 20). **Required.**                                  |
| `--message <text>` | Optional personal note shown in the email body (≤2000 chars).                           |
| `--subject <text>` | Optional custom subject line (≤200 chars).                                              |
| `--token-id <id>`  | Reuse a single existing token for every recipient instead of minting one per recipient. |

**JSON output (success):**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": true,
  "data": {
    "presentationId": "01a3b...",
    "sent": [
      {
        "email": "alice@x.com",
        "tokenId": "01a3c...",
        "resendMessageId": "re_...",
        "shareUrl": "https://app.slideless.ai/share/01a3b...?token=..."
      }
    ],
    "failed": [
      { "email": "bob@bad", "code": "invalid-email", "message": "Not a valid email address." }
    ],
    "summary": { "total": 2, "sent": 1, "failed": 1 }
  }
}
```

`failed[]` contains **per-recipient** failures inside an otherwise-successful call — `success: false` only fires when zero emails could be attempted (auth, not-found, too-many-recipients, etc.).

Error codes specific to `share-email`:

| Code                  | Meaning                 | `nextAction`                                                                        |
| --------------------- | ----------------------- | ----------------------------------------------------------------------------------- |
| `missing-recipients`  | No `--to` flags         | Add at least one recipient and retry                                                |
| `too-many-recipients` | >20 recipients          | Split into batches of ≤20                                                           |
| `message-too-long`    | `--message` >2000 chars | Shorten or omit                                                                     |
| `invalid-email`       | Bad email syntax        | Returned in `failed[]`, not top-level. Show the user which addresses were rejected. |
| `email-send-failed`   | Resend API failure      | Returned in `failed[]`. Retry once; if persistent, Resend is having issues.         |

See the [share-via-email guide](/guides/sharing-via-email) for worked examples.

### `slideless list`

List every presentation you can access — decks you own and decks shared with you. Each row carries a `role` column (`owner` or `dev`).

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless list
slideless list --json
```

### `slideless get <presentationId>`

Show full metadata for a single presentation.

* For owners: versions, tokens, collaborators, view counts.
* For dev collaborators: a read-only view with a `role: 'dev'` marker.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless get 01HXYZ...
slideless get 01HXYZ... --json
```

### `slideless pin <presentationId> <tokenId>`

Change a share token's version mode — pin it to a specific version or put it back on auto-follow-latest. Only the presentation owner can pin.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless pin 01a3b... 01a3c... --to-version 3    # freeze this recipient on v3
slideless pin 01a3b... 01a3c... --latest          # auto-follow future updates
slideless pin 01a3b... 01a3c... --to-version 3 --json
```

Flags:

| Flag               | Purpose                                                             |
| ------------------ | ------------------------------------------------------------------- |
| `--to-version <N>` | Pin to version N. Mutually exclusive with `--latest`.               |
| `--latest`         | Follow the current version. Mutually exclusive with `--to-version`. |

Recipients cannot change their own version — there is no query-param override. Use `slideless get <presentationId>` to inspect every token's current `versionMode`. See [Versioning](/concepts/versioning).

## Marketplace

The [marketplace](/concepts/marketplace) is a public, npm-style catalog of remixable presentations. These verbs publish, browse, and remix listings:

| Verb              | Job                                               | Key needed?                       |
| ----------------- | ------------------------------------------------- | --------------------------------- |
| `publish`         | Publish a linked deck as a listing                | Yes — `marketplace:publish` scope |
| `unpublish`       | Take a listing down                               | Yes                               |
| `remix`           | Download a listing into a fresh local folder      | No                                |
| `search`          | Browse the public catalog                         | No                                |
| `listing get`     | Show one listing's full detail                    | No                                |
| `listing update`  | Edit a listing's metadata, status, or version pin | Yes                               |
| `star` / `unstar` | Add/remove a star on a listing                    | Yes                               |
| `stars`           | List the listings you've starred                  | Yes                               |

### `slideless publish`

Publish the presentation linked to the **current folder** as a marketplace listing, pinned to its current immutable version. Run from inside a linked deck folder (one with `slideless.json`). Owner-only. The active key needs the `marketplace:publish` scope.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless publish --kind presentation --description "A clean pitch-deck starting point."
slideless publish --kind app --description "A standalone ROI calculator." \
  --slug roi-calculator --tags finance,calculator --category business
slideless publish --kind plan --description "A Next.js + Firebase SaaS build plan." \
  --stack nextjs,firebase,tailwind
slideless publish --kind presentation --interactive --description "A parametric cost-quote deck."
slideless publish --kind presentation --description "..." --version 3   # pin a specific version
```

Flags:

| Flag                               | Purpose                                                                                                                                                                                                                        |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--kind <presentation\|app\|plan>` | **Required.** Categorizes the listing by purpose. `presentation` = the message (a deck you view); `app` = the machine (a self-contained app you operate); `plan` = the blueprint (an agent-executable build plan).             |
| `--interactive`                    | Mark the listing as interactive. Optional. Forced on for `--kind app`; defaults off for `--kind presentation`. Interactivity is a badge, not a category.                                                                       |
| `--description <text>`             | **Required.** One- or two-line catalog summary.                                                                                                                                                                                |
| `--slug <slug>`                    | Override the auto-derived slug. Lowercase, hyphenated, unique.                                                                                                                                                                 |
| `--title <title>`                  | Listing title. Defaults to the presentation's title.                                                                                                                                                                           |
| `--tags a,b,c`                     | Comma-separated search tags.                                                                                                                                                                                                   |
| `--stack a,b,c`                    | Technologies the artifact is built with or built for, as comma-separated lowercase slugs (e.g. `nextjs,firebase,n8n`). One canonical slug per technology; no display names or versions. Especially useful for `plan` listings. |
| `--category <name>`                | Single category bucket.                                                                                                                                                                                                        |
| `--version <N>`                    | Pin to a specific version. Default: the deck's current version.                                                                                                                                                                |

The listing is created `public`. See the [publishing guide](/guides/publishing-to-marketplace).

### `slideless unpublish <slug>`

Remove a listing from the marketplace. The underlying presentation is untouched. Owner-only.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless unpublish q4-pitch-template          # interactive — asks to confirm
slideless unpublish q4-pitch-template --yes    # skip the prompt (scripts, CI)
```

`unpublish` prompts for confirmation before taking the listing down — mirroring [`slideless delete`](/cli/commands#slideless-delete-presentationid). Pass `--yes` to skip the prompt; it is **required** in `--json` / non-interactive mode, where there is no TTY to confirm on.

You must `unpublish` a listing before you can `slideless delete` the presentation behind it.

### `slideless remix <slug> [path]`

Download a listing's files into a **fresh local folder**. No API key required. The folder is written with **no `slideless.json`** — it is not linked to any presentation. Increments the listing's public remix count.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless remix q4-pitch-template               # → ./q4-pitch-template/
slideless remix q4-pitch-template ./my-pitch     # explicit destination
```

The destination must be empty or new. Alongside the deck files, `remix` writes a **`.slideless-remix.json`** marker recording the source slug, version, and title. The first `slideless push` from the folder reads that marker and stores the lineage on the new presentation permanently — so a listing you later publish shows "Remixed from \<original>". The marker is excluded from the uploaded deck; delete it before the first push if you want an uncredited copy.

To turn the remixed folder into your own deck, run `slideless push` from inside it — that first push creates a brand-new presentation you own. See the [remixing guide](/guides/remixing-templates).

### `slideless search [query]`

Browse the public marketplace catalog. No API key required.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless search "pitch deck"
slideless search --kind app --tag finance --sort popular
slideless search --kind plan --stack nextjs --sort popular
slideless search "pricing" --kind presentation --interactive --category business --sort stars --json
```

Flags:

| Flag                               | Purpose                                                                                       |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| `--kind <presentation\|app\|plan>` | Filter to one kind — presentations, apps, or plans.                                           |
| `--interactive`                    | Filter to interactive listings only.                                                          |
| `--tag <tag>`                      | Filter by a search tag.                                                                       |
| `--stack <tech>`                   | Filter by a single technology (e.g. `nextjs`). Matches listings whose tech stack includes it. |
| `--category <name>`                | Filter by category bucket.                                                                    |
| `--sort recent\|popular\|stars`    | Order — newest, most-remixed, most-starred. Default `recent`.                                 |
| `--json`                           | Stable JSON output.                                                                           |

### `slideless listing get <slug>`

Show full detail for one listing — README, tags, counters, file manifest. No API key required. Works for both `public` and `unlisted` listings.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless listing get q4-pitch-template
slideless listing get q4-pitch-template --json
```

The output includes all three public counters — **stars** (`starCount`), **remixes** (`remixCount`), and **views** (`viewCount`). If the listing was published from a remixed deck, it shows **"Remixed from \<original>"** with the source slug (`remixedFromSlug`). If other listings were remixed from it, it lists them under **"Remixes of this"** (the descendants from [`GET /listMarketplaceRemixes`](/api-reference/list-marketplace-remixes)). See [Marketplace › remix lineage](/concepts/marketplace#remix-lineage).

### `slideless listing update <slug>`

Edit a listing you own — metadata, visibility, or version pin. Owner-only.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless listing update q4-pitch-template --description "Updated 8-slide skeleton."
slideless listing update q4-pitch-template --status unlisted
slideless listing update q4-pitch-template --republish-version   # re-pin to current deck version
```

Flags:

| Flag                          | Purpose                                                                                        |
| ----------------------------- | ---------------------------------------------------------------------------------------------- |
| `--title <text>`              | New listing title.                                                                             |
| `--description <text>`        | New catalog summary.                                                                           |
| `--readme <path>`             | Path to a Markdown file used as the listing's long-form README.                                |
| `--tags a,b,c`                | Replace the search tags.                                                                       |
| `--stack a,b,c`               | Replace the listing's tech stack — the technologies it's built with or built for.              |
| `--category <name>`           | Change the category bucket.                                                                    |
| `--status <public\|unlisted>` | Switch discoverability. `public` shows in search; `unlisted` is reachable only by direct slug. |
| `--republish-version`         | Re-pin the listing to the deck's **current** version.                                          |

### `slideless star <slug>` / `slideless unstar <slug>`

Add or remove a star on a listing. A key is required (so stars are attributed to an account). Star count feeds `slideless search --sort stars`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless star q4-pitch-template
slideless unstar q4-pitch-template
```

### `slideless stars`

List the listings you've starred. Requires a key.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless stars
slideless stars --json
```

## Config

### `slideless config show`

List all saved profiles.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless config show
slideless config show --json
```

### `slideless config set`

Save a profile. Same behaviour as `slideless login`.

### `slideless config clear`

Remove config or a specific profile.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
slideless config clear
slideless config clear --profile work-org
```

## Error shape (`--json`)

Every command returns this shape on failure:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "success": false,
  "status": 401,
  "error": { "code": "unauthenticated", "message": "Invalid or missing API key." }
}
```

Common `code` values (share / presentations):

| Code                         | Meaning                                                                                         |
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
| `unauthenticated`            | Missing or invalid API key                                                                      |
| `permission-denied`          | Key valid but lacks the required scope (e.g. `marketplace:publish`), or caller is not owner/dev |
| `not-found`                  | The `presentationId`, `collaboratorId`, or marketplace `slug` doesn't exist                     |
| `slug-taken`                 | A marketplace `--slug` is already in use by another listing                                     |
| `conflict`                   | Push lagging behind server (409). Pull and retry, or pass `--force`.                            |
| `collaborator-limit-reached` | Hit the 10-per-deck cap on concurrent invites                                                   |
| `invalid-email`              | Email in `--email` is not a valid address                                                       |
| `payload-too-large`          | File or total deck size exceeds the plan cap                                                    |
| `hash-mismatch`              | Upload bytes don't match declared sha256 (or pull blob doesn't match manifest)                  |
| `blobs-missing`              | `commit` was called with a manifest referencing an unuploaded hash                              |
| `too-many-files`             | Manifest exceeds the plan's max file count                                                      |
| `invalid-argument`           | Request body is malformed                                                                       |
| `network-error`              | Couldn't reach the backend                                                                      |
| `internal`                   | Backend 5xx                                                                                     |

Additional codes for the [OTP auth flow](/cli/auth) (`slideless auth ...`):

| Code                                                                        | Meaning                                    | `nextAction`                                            |
| --------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------- |
| `USER_ALREADY_HAS_ORGANIZATION`                                             | Signup email already owns an org           | Switch to `login-request`                               |
| `USER_NOT_FOUND` / `USER_HAS_NO_ORGANIZATION`                               | Login email has no account (or no org yet) | Switch to `signup-request`                              |
| `OTP_RESEND_COOLDOWN`                                                       | Called `-request` twice within 30 s        | Wait `details.retryInSeconds`, retry                    |
| `OTP_EXPIRED` / `OTP_NOT_FOUND` / `OTP_ALREADY_USED` / `OTP_LOCKED_OUT`     | Code is stale or bad                       | Run the matching `-request` again                       |
| `OTP_INVALID`                                                               | Wrong 6 digits                             | `details.attemptsRemaining` shows how many tries remain |
| `OTP_PURPOSE_MISMATCH`                                                      | Signup code used for login (or vice versa) | Run the matching `-request`                             |
| `LOGO_TOO_LARGE` / `LOGO_INVALID_FORMAT` / `LOGO_DECODE_FAILED`             | Logo rejected                              | Use a PNG/JPEG/WebP/SVG under 2 MB                      |
| `BRAND_COLOR_INVALID` / `COMPANY_NAME_TOO_LONG` / `INVALID_EXPIRES_IN_DAYS` | Optional-field validation                  | Adjust or omit the flag                                 |

Every OTP error includes a `nextAction` string designed to be acted on programmatically (e.g. `jq -r '.error.nextAction'`).
