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 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
npm install -g uploadadOr run it without installing:
npx uploadad --helpbun works too: bun install -g uploadad or bunx uploadad.
Authentication
uploadad loginOpens 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): run uploadad login --key ua_live_..., or uploadad login --key to be prompted for it.
Every command resolves the credential in this order:
- The
--api-keyflag - The
UPLOADAD_API_KEYenvironment variable - 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:
uploadad whoami # show the authenticated account
uploadad logout # sign out and remove stored credentialsUploading creatives
Pass one or more file paths. Each file becomes its own upload job:
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:
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:
uploadad upload banner.png promo.mp4 --waitWith --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
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 jobList 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
uploadad creatives list # --limit, --cursor, and --all paginate
uploadad creatives rm <id> [<id> ...]Managing copy variants
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:
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 rmTools
Beyond uploads, creatives, and copy, the CLI can call any tool in the upload.ad catalog (the same set the MCP server exposes: campaigns, ads, audiences, automations, and more). List what is available:
uploadad tools # catalog table: name, destructive, description
uploadad tools --json # full catalog with JSON schemasThe 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:
uploadad call get_campaigns --args '{ "platform": "meta" }'
uploadad call get_campaigns --arg platform=metaWith --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:
uploadad call delete_ad --arg adId=... --arg platform=meta --yesScripting
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:
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:
uploadad mcpSee the MCP setup guide 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.