gads is a safe, scriptable, agent-friendly command-line interface for the Google Ads API.
It combines Google's official Python client with practical commands for account discovery,
GAQL reporting, campaign operations, App Campaign creation, asset uploads, and generic
mutations.
The repository also includes a Codex skill, so an AI agent can use the same guarded workflow instead of improvising direct API calls.
Important
This is an independent open-source project. It is not affiliated with or endorsed by Google. Google Ads is a trademark of Google LLC.
Every mutating command has three explicit modes:
| Mode | Flag | What happens |
|---|---|---|
| Plan | none | Builds and validates the protobuf request locally, then prints it. No credentials or network request are needed. |
| Validate | --validate-only |
Sends the request to Google Ads for validation without applying it. |
| Execute | --execute |
Applies the mutation and records a sanitized summary in the local audit log. |
Plan mode is the default. A pasted command cannot spend money merely because it parsed.
New App Campaigns are always created in PAUSED status, leaving activation as a separate,
reviewable action.
- Complete a single-user OAuth flow and create a private
google-ads.yaml - Manage multiple non-secret profiles
- Discover directly accessible accounts and recursive manager hierarchies
- Run and validate arbitrary GAQL; discover valid Google Ads fields
- Output tables, JSON, JSONL, or CSV
- Run curated account, campaign, ad group, ad, daily, conversion, per-asset, and ad-network reports
- Inspect campaigns, budgets, ad groups, ads, assets, and conversion actions
- Pause, enable, or remove campaigns and update daily budgets
- Create an atomic App Campaign with budget, criteria, ad group, app ad, and assets
- Upload image assets and create YouTube assets
- Inspect an App Ad's real assets and edit them in place without rebuilding the ad group
- Report account funding, tax-adjusted balance, and spend runway
- Review account change history (who changed what, when)
- Resolve geographic and language constants
- Apply versioned
GoogleAdsService.MutateYAML manifests - Keep a non-secret JSONL audit trail with deterministic plan hashes
You need:
- Python 3.11 or newer.
uv.- A Google Ads manager account with a developer token.
- A Google Cloud project with the Google Ads API enabled.
- An OAuth 2.0 Desktop app client JSON.
- A Google user that can access the target Ads account.
Google assigns every developer token an access level:
| Access level | Accounts | Daily operations |
|---|---|---|
| Test | Test accounts | 15,000 |
| Explorer | Test and production | 2,880 production; 15,000 test |
| Basic | Test and production | 15,000 |
| Standard | Test and production | Unlimited for most services |
See Google's current developer-token guide and access-level table before relying on these limits.
On macOS with Homebrew:
brew install uvFor Linux, Windows, or other installation methods, use the official uv instructions.
Clone or download the repository, then enter its root directory:
cd google-ads-cli./scripts/install-local.sh
gads --versionThe installer creates an editable isolated uv tool and links skills/google-ads into your
Codex skills directory. To install only the CLI:
uv tool install .For repository development without a global command:
uv sync --dev
uv run gads --helpSign in to a Google Ads manager account, open its API Center, and apply for a developer token. “Manager account” is an account type (MCC), not an administrator role on a client account. If API Center says it is available only to manager accounts, switch the Google Ads account selector to an MCC or create one first.
New applications may receive Explorer access automatically; otherwise they begin with test account access. Explorer, Basic, and Standard access can call production accounts within their respective limits. See Google's current developer-token access documentation.
In Google Cloud:
- Create or select a project.
- Enable the Google Ads API.
- Configure the OAuth consent screen.
- Create an OAuth client with application type Desktop app.
- Download the client JSON and keep it outside this repository.
When the OAuth app publishing status is Testing, add every Google user that will authorize the CLI under Google Auth Platform → Audience → Test users. Being an MCC administrator does not automatically make a user an OAuth test user. Testing authorizations for the Ads scope expire after seven days, including offline refresh tokens; publish the app to Production when you need durable authorization and complete verification if Google requires it.
Google's OAuth guide describes the current console flow.
Run:
gads auth login \
--client-secrets /absolute/path/to/client_secret.json \
--login-customer-id 1111111111 \
--customer-id 2222222222The developer token is requested through a hidden prompt. The command opens Google's OAuth
page with the account picker enabled, writes a credentials YAML with file mode 0600, and
creates a non-secret CLI profile. Choose a Google user that can directly access the MCC
specified by --login-customer-id.
customer_idis the client account whose campaigns and data you manage.login_customer_idis the manager account used to reach that client. Omit it when you access the client directly.- Hyphenated IDs are accepted and normalized to 10 digits.
Keep these four identities separate:
| Item | Purpose |
|---|---|
| Developer-token owner | The MCC whose API Center issued the token. |
| OAuth client | The Desktop app in a Google Cloud project. |
| OAuth Google user | The human user granting access; it must have Google Ads access. |
| Login/target customers | The MCC request header and the client whose data is queried. |
For a shell that cannot open a browser, add --no-browser, open the printed URL on a browser
that can reach the same local callback, and complete the flow.
gads auth test
gads accounts accessible
gads accounts hierarchy --manager-id 1111111111
gads accounts showaccounts accessible lists direct access only. Use accounts hierarchy to find clients
beneath a manager, and seed it with that manager's ID rather than the client profile ID.
Global options go before the command group:
gads --profile default --customer-id 2222222222 --format json campaigns listUseful reads:
gads --format json accounts show
gads --format json budgets list
gads --format json adgroups list
gads --format json ads list
gads --format json assets list
gads --format json conversions listCurated reports:
gads reports list
gads --format json reports run campaigns --date-range LAST_30_DAYS
gads --format csv reports run daily --date-range 2026-07-01:2026-07-28Raw GAQL:
gads query validate --file examples/app-campaign-performance.gaql
gads --format jsonl query run \
--file examples/app-campaign-performance.gaql \
--limit 1000Discover field names instead of guessing:
gads --format json fields describe campaign.app_campaign_setting.app_id
gads --format json fields search 'metrics.%' --limit 50Use the same command through plan, validation, execution, and verification:
# 1. Local plan; no account is changed
gads campaigns set-status 123456789 PAUSED
# 2. Google validates it; no account is changed
gads campaigns set-status 123456789 PAUSED --validate-only
# 3. Apply it
gads campaigns set-status 123456789 PAUSED --execute
# 4. Read it back
gads campaigns get 123456789--execute and --validate-only cannot be combined. Prefer PAUSED for normal shutdowns;
removal is a distinct and generally irreversible Google Ads lifecycle action.
Update a budget using the account's currency:
gads budgets set-amount 987654321 50
gads budgets set-amount 987654321 50 --validate-only
gads budgets set-amount 987654321 50 --executeAlways run gads accounts show first and confirm the account currency.
This generic iOS example creates a local plan only:
gads --format json campaigns create-app \
--name "Example App · US · Install" \
--app-id 000000000 \
--app-store APPLE_APP_STORE \
--daily-budget 50 \
--goal installs \
--target-cpa 2.50 \
--headline "Create Something New" \
--headline "Your Ideas, Made Simple" \
--description "Turn an idea into something worth sharing." \
--description "Create, refine, and share in just a few steps." \
--location 2840 \
--language 1000Replace the example App Store ID, copy, targeting, budget, and bid with real reviewed values.
For Android, use the package name as --app-id and pass
--app-store GOOGLE_APP_STORE.
The plan creates a non-shared budget, a paused multi-channel campaign, location/language
criteria, an enabled ad group, and an enabled App Ad in one atomic request. Repeat the command
with --validate-only, inspect the result, then use --execute.
Before enabling spend, verify billing, conversion tracking, account currency, policy status, targeting, assets, and the app-store record. For in-app action or value bidding, inspect the real conversion resources first:
gads --format json conversions listImage uploads are checked locally for supported content and dimensions:
gads assets upload-image /absolute/path/creative.png --name "US Creative 1"
gads assets upload-image /absolute/path/creative.png \
--name "US Creative 1" \
--validate-only
gads assets upload-image /absolute/path/creative.png \
--name "US Creative 1" \
--executeImage bytes are redacted from plans and represented by a SHA-256 digest.
Create a YouTube asset:
gads assets create-youtube VIDEO_ID --name "US Demo 15s"Google Ads assets are generally immutable. Stop one from serving by changing the ad or association that uses it.
An App Ad cannot be duplicated within its ad group or removed, but its asset fields can be updated. Rebuilding the ad group for every creative change is unnecessary — and it leaves undeletable ads behind.
Read what the ad actually carries, with slot fill and orientation coverage:
gads ads assets 111222333444ad_group_ad_asset_view is not the source of truth: it retains historical associations
and can report more assets than the ad has. These commands read app_ad.* instead.
Asset fields are whole-field replacements, so an update_mask on app_ad.images drops
anything left out of the payload. set-assets reads the current assets first and applies
your delta on top:
gads ads set-assets 111222333444 --add-video 555000111222 --remove-video 555000333444
gads ads set-assets 111222333444 --add-video 555000111222 --validate-only
gads ads set-assets 111222333444 --add-video 555000111222 --executeUse --set-image, --set-video, --set-headline, or --set-description to replace a
whole list. Per-ad-group caps, duplicate assets, removing an asset the ad does not have,
and stripping every visual asset are all rejected before anything reaches the API.
Each change triggers an ad review, so batch creative edits into one call.
gads billing show
gads billing show --tax-rate 0.06account_budget reports the net spendable amount. A prepay top-up shown as a gross
figure in the web UI arrives here already divided by the local tax rate, so the real
runway is shorter than the UI number suggests — --tax-rate prints a gross-equivalent
column to reconcile the two. Runway defaults to the summed daily budgets of enabled
campaigns; override it with --daily-budget.
Promotional credits ("spend X, get X") are not exposed by the API at all.
gads changes list --days 14
gads changes list --days 7 --resource-type CAMPAIGN_BUDGET
gads changes list --campaign-id 123456789 --limit 500change_event retains 30 days, requires a bounded date window, and requires a LIMIT;
this command supplies all three.
Dedicated commands cover common operations. The versioned manifest escape hatch covers
resources available through GoogleAdsService.Mutate:
gads --format json mutate schema
gads mutate apply examples/pause-campaign.yaml
gads mutate apply examples/pause-campaign.yaml --validate-only
gads mutate apply examples/pause-campaign.yaml --executeManifest example:
label: pause-one-campaign
customer_id: "1234567890"
api_version: v25
partial_failure: false
response_content_type: RESOURCE_NAME_ONLY
operations:
- resource: campaign
action: update
data:
resourceName: customers/1234567890/campaigns/111
status: PAUSED
update_mask:
- statusUse protobuf JSON field names, string enum labels, and explicit update masks. Use temporary negative resource IDs when later operations depend on resources created earlier in the same atomic request.
Add another profile without repeating OAuth:
gads config init \
--name production \
--credentials /absolute/path/to/google-ads.yaml \
--login-customer-id 1111111111 \
--customer-id 2222222222 \
--api-version v25Select it with --profile production or GADS_PROFILE=production.
Environment-only authentication supports:
GOOGLE_ADS_DEVELOPER_TOKEN
GOOGLE_ADS_CLIENT_ID
GOOGLE_ADS_CLIENT_SECRET
GOOGLE_ADS_REFRESH_TOKEN
GOOGLE_ADS_LOGIN_CUSTOMER_ID
GOOGLE_ADS_JSON_KEY_FILE_PATH
GOOGLE_ADS_USE_APPLICATION_DEFAULT_CREDENTIALS
GADS_CUSTOMER_ID
GADS_API_VERSION
Use only the credential group for your chosen auth method. Prefer a private credentials file over shell history or a checked-in dotenv file.
Inspect local paths without revealing secret contents:
gads config path
gads config show
gads audit list./scripts/install-local.sh links the bundled google-ads skill into the Codex skills
directory. A request such as:
Use $google-ads to report campaign performance for the last 30 days.
causes Codex to follow the same read → plan → validate → execute → verify workflow. The skill
does not bypass --execute, Google authentication, developer-token access, or account
permissions.
| Symptom | Likely cause and next check |
|---|---|
| “API Center is only available to manager accounts” | The selected Google Ads account is a client account. Switch to or create a manager account (MCC); client-account admin permission is not the same thing. |
OAuth 403 access_denied, app is being tested |
Add the Google user under Google Auth Platform → Audience → Test users, save, then retry. |
DEVELOPER_TOKEN_NOT_APPROVED |
The token cannot access that production account. Check its API Center access level. |
USER_PERMISSION_DENIED |
The OAuth user cannot access the selected client or manager. Re-run login and choose the intended user; gads auth test also checks direct MCC access. |
| Login-customer error | login_customer_id is not a manager above the client, or the IDs were reversed. |
OAuth invalid_grant |
The refresh token was revoked, the OAuth client changed, or the consent grant expired. Run gads auth login again. |
ServiceUnavailable, DNS, or “No route to host” |
Check VPN/proxy/firewall and IPv6 routing. macOS automatically uses the system resolver; elsewhere try GRPC_DNS_RESOLVER=native gads auth test. |
| “Missing customer ID” | Pass --customer-id before the command or store it in the selected profile. |
| A global option is rejected | Put --profile, --customer-id, --api-version, and --format before the command group. |
| Plan works but validation fails | Read the structured Google Ads error and request_id; then inspect the referenced field with gads fields describe. |
Do not paste credentials into an issue. See SECURITY.md for private reporting.
- Secrets, OAuth files, keys, dotenv files, caches, and build output are ignored by Git.
- Credentials and profiles are written with mode
0600where the operating system supports POSIX permissions. - The CLI never prints credential contents.
- Audit records store customer ID, API version, mode, outcome, operation count, plan hash, request ID, and sanitized errors—not tokens or full mutation payloads.
- Mutations do not add blanket retries because Google Ads mutates do not provide a universal idempotency key.
- Unit tests require no Google credentials and never contact or mutate a Google Ads account.
- A pre-commit hook blocks real account identifiers — IDs, balances, emails, live ad copy — from entering this public repository. See CONTRIBUTING.md.
Review SECURITY.md before using the CLI in production. You remain responsible
for account permissions, policy compliance, spend, and every command executed with
--execute.
Version 0.1.0 defaults to Google Ads API v25 through official Python client 31.x. Run
gads --version to see every API schema available in the installed environment. Google Ads
versions have fixed sunset dates, so keep the CLI and lock file current.
uv sync --dev
uv run ruff check .
uv run ruff format --check .
uv run pytest --cov=google_ads_cli --cov-fail-under=55
git ls-files -z | xargs -0 uv run detect-secrets-hook --baseline .secrets.baseline
uv buildTests use only local protobuf schemas and fixtures. Live --validate-only or --execute
checks require your own account and are intentionally excluded from CI.
The main code is in src/google_ads_cli/; CLI tests are in tests/; reusable examples are in
examples/; and the Codex skill is in skills/google-ads/.
See CONTRIBUTING.md, SECURITY.md, and CHANGELOG.md before opening a contribution.
Licensed under the Apache License 2.0.