Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🍎 Apple Ads CLI

The best CLI for Apple Ads. Agent-first, JSON-native, spend-safe, and built to work out of the box.

Go CLI Apple Ads API License: MIT

# Inspect
$ ads campaigns list --json

# Dry-run
$ ads keywords add 123 456 --text "photo editor,image filter" --match EXACT --bid 1.75 --json

# Apply
$ ads keywords add 123 456 --text "photo editor,image filter" --match EXACT --bid 1.75 --apply --json

Used by

Spottable — marketplace alerts and deal intelligence for finding better local deals.

✨ Why ads

Apple Ads should have a great CLI. Now it does.

No Python project. No Node stack. No browser automation. No dashboard clicking. Just one Go CLI for the full inspect -> plan -> apply -> verify loop.

  • 🤖 Agent-first - JSON output, stable commands, predictable workflows.
  • 🛡️ Spend-safe - mutating commands dry-run first and require --apply.
  • Works out of the box - installs as ads; config is local and non-interactive.
  • 📦 Single compiled binary - no runtime dependency chain after install.
  • 🔎 Full escape hatch - ads api reaches raw Apple Ads API endpoints.
  • 🔐 Secret-friendly - first-class 1Password hydration.
  • 🧭 Four-campaign aware - Brand, Category, Competitor, Discovery.

This is unofficial. It is also the CLI Apple Ads should have had.

🚀 Coverage

  • Campaigns, ad groups, keywords, negative keywords, reports, budgets, geo, ads, creatives, ACLs.
  • Apple Ads Platform API v1 — opt in per context (ads config api v1) with all new endpoints: search term popularity, recommendations, suggestions, change history, ad accounts, shared budgets, impression share.
  • Dry-run mutations everywhere spend can change.
  • Clean JSON for agents, scripts, and dashboards.
  • Raw API access without writing OAuth code.
  • Local config in ~/.apple-ads-cli.
  • 1Password setup that avoids pasting secrets into chat.

⚡ Quick Start

Install 🧰

Download a prebuilt macOS binary:

arch="$(uname -m)"
case "$arch" in
  arm64) target="arm64" ;;
  x86_64) target="amd64" ;;
  *) echo "unsupported macOS arch: $arch" >&2; exit 1 ;;
esac

curl -L -o ads.zip "https://github.com/dannolan/apple-ads-cli/releases/latest/download/ads_darwin_${target}.zip"
unzip -q -o ads.zip ads

install -m 0755 ads /usr/local/bin/ads
ads --help

Or build locally with Go:

go install github.com/dannolan/apple-ads-cli/cmd/ads@latest
ads --help

From a checkout:

go build -o ads ./cmd/ads
./ads --help

Homebrew builds from source:

brew tap dannolan/tap
brew install apple-ads-cli
ads --help

Configure 🔑

Create Apple Ads API credentials in Apple Ads account settings. You need:

  • Org ID
  • Client ID
  • Team ID
  • Key ID
  • EC private key PEM path

Generate an EC key if needed:

openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem

Save credentials and an app profile:

ads config init \
  --org-id 123456 \
  --client-id "$APPLE_ADS_CLIENT_ID" \
  --team-id "$APPLE_ADS_TEAM_ID" \
  --key-id "$APPLE_ADS_KEY_ID" \
  --private-key ./private-key.pem

ads config app add \
  --app-id 1234567890 \
  --name "My App" \
  --countries US,CA,GB \
  --currency USD \
  --bid 1.50 \
  --cpa-goal 5.00

Verify access with read-only checks:

ads config test --json
ads smoke --json
ads campaigns list --json

Multiple contexts 🔀

Manage several accounts side by side, like doctl contexts. Each context is an isolated credentials and app store. The default context maps to the config directory root, so existing setups keep working with no migration.

ads context list --json          # show all contexts and which is active
ads context current --json       # print the active context name
ads context create work          # create a context and make it active
ads config init --org-id ...     # writes to the active context
ads config app add --app-id ...  # app profiles are per context too
ads context use default          # switch back
ads context remove work          # delete a context and its local config

Scope a single command to another context without switching — --context overrides the active context for that command only and never changes it:

ads config init --context work --org-id ...
ads campaigns list --context work --json

Check the current context anytime: ads context current --json, ads context list --json, or ads config show --json (which reports both active_context and the context the command resolved to).

🆕 Apple Ads Platform API v1

Apple's Campaign Management API v5 is sunsetting January 26, 2027 in favor of the new Apple Ads Platform API v1 (https://api.ads.apple.com/v1). ads supports both. The default stays v5 so existing setups keep working; flip a context to v1 when you're ready to migrate:

ads config api v1 --json             # per-context API version switch
ads config ad-account 123456 --json  # v1 replaces orgId with adAccountId
ads config show --json               # confirm api_version + ad_account_id

Or override for a single command with --api v1 (no config change):

ads campaigns list --api v1 --json

On v1 the CLI automatically uses the flattened /{entity}/query endpoints, the result envelope, adAccountId headers, money objects, bidStrategy.bid bids, and synchronous reports. New v1-only commands:

# Search term popularity ("the terms stuff") — trending terms by genre + country
ads terms --country US --genre PRODUCTIVITY --granularity WEEKLY_SUN_SAT --json
ads terms --country GB --genre GAMES --granularity MONTHLY --days 30 --json

# Recommendations — query, then apply or dismiss
ads recommendations keyword query --json
ads recommendations keyword apply --id rec-kw-001 --apply --json
ads recommendations target-cpa query --json
ads recommendations budget dismiss --id rec-budget-001 --apply --json

# Suggestions (read-only)
ads suggestions keywords --body '{"filters":[...]}' --json
ads suggestions target-cpa --json

# Change history (audit trail with field-level before/after)
ads change-history query --entity-type Campaign --json
ads change-history detail Campaign.444555666.txn_abc123def456 --json

# Ad account management
ads ad-accounts list --json
ads ad-accounts get 123456 --json
ads ad-accounts resources --resource-type CONTENT_PROVIDER --json

# Insights + shared budgets
ads insights impression-share <campaign-id> --days 14 --json
ads shared-budgets list --json
ads shared-budgets get 777890001 --json

All wrapped commands (campaigns, adgroups, keywords, reports, budget, ads, etc.) are v1-aware: in v1 mode they hit the Platform API shapes above automatically. Dry-run protection and --json output behave identically.

🔐 1Password Setup

Do not paste Apple Ads credentials into agent chat. Store them in 1Password and hydrate the local CLI config with the 1Password CLI (op).

Recommended layout:

1Password object Name Fields
API credential item Apple Ads API org_id, client_id, team_id, key_id, ad_account_id (optional), api_version (optional)
Document Apple Ads API Private Key EC private key PEM

Then run:

ads config from-1password \
  --vault "Private" \
  --item "Apple Ads API" \
  --key-document "Apple Ads API Private Key" \
  --app-id 1234567890 \
  --app-name "My App" \
  --countries US \
  --currency USD \
  --json

ads smoke --json

ads smoke is read-only. It verifies OAuth, /me, supported countries, active app config, app eligibility, and campaign listing.

🧠 Core Workflows

Inspect 👀

ads config show --json
ads acl me --json
ads campaigns list --json
ads campaigns audit --json
ads campaigns health --days 3 --json
ads account snapshot --days 7 --json

Report 📈

ads reports summary --days 7 --json
ads reports keywords <campaign-id> --days 14 --json
ads reports search-terms <campaign-id> --days 14 --json
ads reports impression-share <campaign-id> --days 14 --json
ads reports impression-share <campaign-id> --days 14 --apply --json
ads reports diagnose <campaign-id> --days 7 --json

Plan 🧾

Mutating commands return a dry-run payload until --apply is present.

ads campaigns setup --prefix "My App" --countries US --daily-budget 50 --json
ads keywords add <campaign-id> <adgroup-id> --text "term one,term two" --match EXACT --bid 1.50 --skip-existing --json
ads keywords add-negatives <campaign-id> --text "bad term" --match EXACT --json
ads campaigns pause <campaign-id> --json

Apply ✅

ads keywords add <campaign-id> <adgroup-id> \
  --text "term one,term two" \
  --match EXACT \
  --bid 1.50 \
  --apply \
  --json

Verify 🔍

ads keywords list <campaign-id> <adgroup-id> --json
ads reports summary --days 1 --json

🗺️ Command Map

Area Commands
Config ads config init, show, test, api, ad-account, app add, app list, app use, from-1password
Context ads context list, use, current, create, remove
Access ads acl list, me, search-apps, eligibility, countries
Campaigns ads campaigns list, get, audit, health, setup, create, update, pause, enable, delete
Ad groups ads adgroups list, create, pause, enable, delete
Keywords ads keywords list, add, add-negatives, find, update-bid, pause, enable, delete, list-negatives, delete-negative
Reports ads reports summary, adgroups, keywords, search-terms, ads, adgroup-keywords, adgroup-search-terms, impression-share, diagnose, bid-recommendations, custom, custom-list, custom-get
Account ads account snapshot
Budget ads budget list, get, status, create
Geo ads geo search, show, set
Ads and creatives ads ads list, create, delete, creative, creatives, product-pages, rejections
Terms (v1) ads terms
Recommendations (v1) `ads recommendations keyword
Suggestions (v1) `ads suggestions keywords
Change history (v1) `ads change-history query
Ad accounts (v1) `ads ad-accounts list
Insights (v1) ads insights impression-share
Shared budgets (v1) `ads shared-budgets list
Escape hatch ads api <method> <path>

Print the live command manifest:

ads manifest --json

💻 Examples

Campaign Management 🎯

ads campaigns audit --json
ads campaigns setup --prefix "My App" --countries US --daily-budget 50 --json
ads campaigns setup --prefix "My App" --countries US --daily-budget 50 --apply --json
ads campaigns update <campaign-id> --body '{"dailyBudgetAmount":{"amount":"20.00","currency":"AUD"}}' --apply --json
ads campaigns rename <campaign-id> --name "ARCHIVED - Discovery" --json
ads campaigns set-budget <campaign-id> --amount 20 --json
ads campaigns set-countries <campaign-id> --countries AU,US --json

Keyword Operations 🔑

ads keywords list <campaign-id> <adgroup-id> --json
ads keywords add <campaign-id> <adgroup-id> --text "brand,my app" --match EXACT --bid 1.50 --json
ads keywords add-negatives <campaign-id> --text "free coins,testflight" --match EXACT --apply --json
ads keywords find --text "photo" --json
ads adgroups set-bid <campaign-id> <adgroup-id> --bid 2.00 --json
ads keywords set-bid <campaign-id> <adgroup-id> <keyword-id> --bid 2.25 --json
ads keywords update-bid <campaign-id> <adgroup-id> <keyword-id> --bid 2.25 --apply --json

Reporting 📊

ads reports summary --days 7 --json
ads reports adgroups <campaign-id> --days 7 --json
ads reports keywords <campaign-id> --days 14 --json
ads reports search-terms <campaign-id> --days 14 --json
ads reports ads <campaign-id> --days 14 --json
ads reports adgroup-keywords <campaign-id> <adgroup-id> --days 14 --json
ads reports adgroup-search-terms <campaign-id> <adgroup-id> --days 14 --json
ads reports bid-recommendations <campaign-id> <adgroup-id> --json
ads reports impression-share <campaign-id> --days 14 --json

ads reports impression-share is an async custom report wrapper. It returns a dry-run /custom-reports payload unless --apply is present.

Use --table for human-readable scans while keeping --json for agents:

ads campaigns list --table
ads reports summary --days 7 --table
ads keywords list <campaign-id> <adgroup-id> --table

Budget, Geo, And Ads 💸

ads budget status --json
ads budget create --name "Q3" --amount 5000 --start 2026-07-01 --apply --json

ads geo search --query "California" --json
ads geo set <campaign-id> --countries US,CA --apply --json

ads ads product-pages --json
ads ads rejections --body '{}' --json

Raw Apple Ads API 🧪

Use this when Apple exposes something before the typed CLI wraps it. Same auth, same config, same dry-run safety for mutating calls.

ads api GET /campaigns --query limit=100 --json
ads api POST /reports/campaigns --body @body.json --json
ads api PUT /campaigns/123 --body '{"campaign":{"status":"PAUSED"}}' --json
ads api PUT /campaigns/123 --body '{"campaign":{"status":"PAUSED"}}' --apply --json
ads api GET /me --no-org-context --json

🧪 Optimization

ads optimize generates an agent-readable optimization plan for weekly search-term maintenance. It keeps the expensive decisions visible: promote winners, block losers, and prevent Discovery from competing with your exact campaigns.

ads optimize --days 14 --json

Recommended policy:

  1. Pull Discovery search-term reports.
  2. Treat search terms with at least 2 installs and CPA at or below the app goal as winners.
  3. Promote winners as exact keywords in Brand, Category, or Competitor campaigns.
  4. Add promoted winners as negatives in Discovery to prevent overlap.
  5. Treat search terms with spend and no installs as losers.
  6. Add losers as negative keywords to the relevant campaign.

🤖 Agent Contract

Agent-first means one strict loop:

  1. Inspect account and app state.
  2. Run reports needed for the decision.
  3. Produce dry-run mutation payloads without --apply.
  4. Apply only after the plan matches intent.
  5. Verify with list or report commands.

The canonical guide is SKILL.md. If you are packaging this for an agent runtime, use the bundled skill at skills/apple-ads-cli/SKILL.md.

🧰 Configuration Files

~/.apple-ads-cli/
|-- context.json        # active context marker ({"active": "default"})
|-- credentials.json    # Apple Ads API credentials for the default context; redacted by `ads config show`
|-- apps.json           # App profiles, active app, countries, bid, and CPA goal for the default context
|-- private-key.pem     # Optional local key path when hydrated from 1Password
`-- contexts/
    |-- work/           # named contexts hold their own credentials.json + apps.json
    |   |-- credentials.json
    |   `-- apps.json
    `-- personal/
        |-- credentials.json
        `-- apps.json

The default context lives at the config root for backward compatibility. Override the location with --config-dir, or pick a context with ads context use <name> / --context <name>.

🛠️ Development

go test ./...
go build ./cmd/ads

Release and publishing notes live in docs/release.md. Wrapped endpoint coverage is tracked in docs/api-audit.md.

📄 License

MIT

About

Agent-first Go CLI for Apple Ads

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages