# CLI

The `uploadad` CLI wraps the REST API for terminals, scripts, and CI pipelines: upload creatives, track upload jobs, and manage copy variants without writing any HTTP code.

**Paid plans only.** The REST API, CLI, SDK, and MCP server are part of every paid plan; the free trial covers the app itself. You can [subscribe](https://upload.ad/dashboard/settings/billing) at any time, including during the trial. A workspace without a plan is refused with `402 subscription_required` (`-32002` over MCP), and an agent that signs in without one is turned away at the authorization screen.

## Install

```bash
npm install -g uploadad
```

Or run it without installing:

```bash
npx uploadad --help
```

bun works too: `bun install -g uploadad` or `bunx uploadad`.

## Authentication

```bash
uploadad login
```

Opens upload.ad in your browser: sign in, approve the device, and you are done. The credential is stored in `~/.config/uploadad/config.json`, along with `--base-url` if you passed one, so later commands talk to the same server. CLI access requires a paid plan.

For headless environments, authenticate with an API key instead (create one at [upload.ad/dashboard/settings/api-keys](https://upload.ad/dashboard/settings/api-keys)): run `uploadad login --key ua_live_...`, or `uploadad login --key` to be prompted for it.

Every command resolves the credential in this order:

1. The `--api-key` flag
2. The `UPLOADAD_API_KEY` environment variable
3. The config file written by `uploadad login`

For CI, set `UPLOADAD_API_KEY` as a secret and skip `login` entirely. To verify which account you are authenticated as, or to sign out:

```bash
uploadad whoami   # show the authenticated account
uploadad logout   # sign out and remove stored credentials
```

## Uploading creatives

Pass one or more file paths. Each file becomes its own upload job:

```bash
uploadad upload banner.png promo.mp4
```

`--platform` picks the ad account the files are pushed to (`meta` is the default, or `tiktok`) and `--folder` files the resulting creatives under a library folder:

```bash
uploadad upload story.mp4 --platform tiktok --folder fo_...
```

Uploads process in the background and the command returns immediately with the job ids. Add `--wait` to block until every file is `completed` or `failed`, which is usually what you want in scripts:

```bash
uploadad upload banner.png promo.mp4 --wait
```

With `--wait`, `--timeout <seconds>` caps how long the command blocks before giving up (default 600).

You can pass any number of files: the CLI sends them to the API in batches of 25 (the per-request limit) and reports the combined result. The per-file limits still apply: images up to 30 MB, videos up to 200 MB. If a later batch fails after earlier ones have already created jobs, the command prints the ids created so far (in the `created` array of the `--json` error output) so you can retry the rest without orphaning them.

To make a retried invocation safe, pass `--idempotency-key <key>`. Re-running `upload` with the same key (and the same files) replays the original request instead of creating a second set of jobs; each 25-file batch derives a stable sub-key from it. Without the flag, only in-flight network retries are deduplicated, not a fresh re-invocation. The same `--idempotency-key` flag is available on the other mutating commands that create records: `copy add` and `webhook set`.

## Managing upload jobs

```bash
uploadad uploads list                    # newest first, default 100
uploadad uploads list --limit 20
uploadad uploads list --status failed --all   # search the full history
uploadad uploads retry <id>              # re-run a failed upload
uploadad uploads rm <id>                 # dismiss a completed or failed job
```

List commands are cursor-paginated: when more pages remain, the table ends with the cursor to continue from (`--cursor <cursor>`), and `--json` output carries it as `nextCursor`. Pass `--all` to fetch every page in one go; combine it with `--status` to filter across the whole history.

## Managing creatives

```bash
uploadad creatives list                  # --limit, --cursor, and --all paginate
uploadad creatives rm <id> [<id> ...]
```

## Managing copy variants

```bash
uploadad copy list <creativeId>

# Create a variant
uploadad copy add <creativeId> \
  --headline "Summer sale" \
  --primary-text "Save 20% this week only." \
  --description "Limited time offer" \
  --label "variant-a"

# Update an existing variant
uploadad copy add <creativeId> --id <copyId> --headline "Summer sale, extended"

# Delete a variant
uploadad copy rm <creativeId> <copyId>
```

## Webhooks

Manage the workspace webhook endpoint and its delivery log:

```bash
uploadad webhook set https://example.com/hooks/uploadad   # prints the signing secret
uploadad webhook get
uploadad webhook events upload.completed upload.failed    # or: uploadad webhook events all
uploadad webhook test
uploadad webhook deliveries --limit 20
uploadad webhook redeliver <deliveryId>
uploadad webhook rm
```

## Tools

Beyond uploads, creatives, and copy, the CLI can call any tool in the upload.ad catalog (the same set the [MCP server](./mcp.md#tools) exposes: campaigns, ads, audiences, automations, and more). List what is available:

```bash
uploadad tools            # catalog table: name, destructive, description
uploadad tools --json     # full catalog with JSON schemas
```

The JSON catalog carries each tool's `inputSchema`, its result shape as `outputSchema` where declared, a coarse `category`, and the `destructive` and `readOnly` flags.

Call a tool by name, passing arguments as a JSON object or as repeated `key=value` pairs:

```bash
uploadad call get_campaigns --args '{ "platform": "meta" }'
uploadad call get_campaigns --arg platform=meta
```

With `--arg`, each value is parsed as JSON when possible and treated as a string otherwise. Destructive tools (those that spend money or make irreversible changes) require `--yes` to confirm:

```bash
uploadad call delete_ad --arg adId=... --arg platform=meta --yes
```

## Scripting

Every command accepts a `--json` flag that prints the raw API response instead of the human-readable output, ready for `jq`. This covers the read commands (`whoami`, `uploads list`, `creatives list`, `copy list`, `tools`, `webhook get`, `webhook deliveries`) and the mutating ones (`upload`, `uploads retry`, `uploads rm`, `creatives rm`, `copy add`, `copy rm`, `webhook set`, `webhook events`, `webhook rm`, `webhook test`, `webhook redeliver`); `call` always prints its result as JSON:

```bash
uploadad uploads list --json | jq -r '.uploads[] | select(.status == "failed") | .id'
```

Filters like `--status` still apply to the JSON output.

Failures exit with a code that tells you what went wrong, so scripts can branch on `$?` instead of parsing stderr:

| Exit code | Meaning                                                                                                                                                                             |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`       | Success                                                                                                                                                                             |
| `1`       | Unexpected error                                                                                                                                                                    |
| `2`       | Invalid input, whether rejected by the API or client-side (unknown tool name, malformed `--args` JSON, an invalid `--platform`/`--status`, or a non-numeric/out-of-range `--limit`) |
| `3`       | Auth or permission problem, including a missing subscription                                                                                                                        |
| `4`       | Not found                                                                                                                                                                           |
| `5`       | Rate or usage limit hit                                                                                                                                                             |
| `6`       | Timeout or upstream platform outage                                                                                                                                                 |

The API's machine-readable error code is also printed in brackets at the end of the error message, e.g. `[rate_limited]`.

## Global flags

| Flag               | Description                                                 |
| ------------------ | ----------------------------------------------------------- |
| `--api-key <key>`  | API key, overrides the environment variable and config file |
| `--base-url <url>` | API base URL, default `https://upload.ad`                   |

`UPLOADAD_BASE_URL` also overrides the base URL when set.

## MCP server

The CLI ships the upload.ad MCP server for AI agents:

```bash
uploadad mcp
```

See the [MCP setup guide](./mcp.md) for connecting it to Claude Code, Claude Desktop, or Cursor.

Because this server runs on your machine, it extends a few media tools with local-file inputs: `upload_creatives` also accepts `filePaths`, and `set_creative_poster` / `set_branding_logo` also accept a `filePath`, read from disk and sent for you.
