Google Ads setup

Get Google Ads API access for the Cloud project behind the OAuth client now that developer tokens are gone, register the callback, set the API environment, and connect a first account.

How to give a deployment access to the Google Ads API, what to set in the API environment, and how to connect the first account.

Everything below was done for real on 2026-09-22 against the production Cloud project and a live Google Ads account: the access application, the OAuth connect, a full sync and a pause/enable round trip through the tool registry. The portal labels are the ones that were on screen that day.


There is no developer token any more

Google sunset developer tokens on 2026-09-09. They are no longer issued, the developer-token header is ignored, and a future API version will reject it. Older guides (and the Google Ads UI's old Tools → API Center page) still describe the token application. Skip it.

Access now belongs to the Google Cloud project that owns the OAuth client, meaning the same client Google sign-in, the GA4 / Search Console connector and YouTube already use. There is no env var for it: the project either has an access level or it does not.

1. Brand-verify the Cloud project

Google Auth Platform → Branding. The right-hand Verification status panel must read "Your branding has been verified and is being shown to users". If it does not, submit it from there. It takes minutes, not days.

Brand verification is only required for Basic access and up, but the consent screen shows an unverified warning without it.

The consent screen is shared with sign-in, GA4/GSC and YouTube. Do not change its User type or Publishing status for Google Ads: either one changes all four flows, and a config change can re-trigger OAuth verification.

2. Enable the API and apply for access

  1. APIs & Services → Library → Google Ads API → Enable.
  2. On the API's page, under Access levels, click Manage. That opens console.cloud.google.com/google/ads-apis/overview.
  3. A fresh project sits at Test, which reaches test accounts only. Click Apply for access under Upgrade access level.

There is no form. The click submits the application, the page says it is "under review", and on 2026-09-22 Explorer was granted in about two minutes.

LevelProduction ops/dayHow you get it
Test0 (test accounts)default
Explorer2,880apply; granted automatically in minutes
Basic15,000apply; needs brand verification
Standardunlimitedapply; manual audit

Explorer is enough to run the product. The hourly incremental costs about 150 operations per account per day, so Explorer tops out near twenty accounts.

On 2026-09-24 the production project applied for Basic from the same page. With branding already verified, it is still one click and no form: the page said "under review", and Basic was granted within minutes. Production now has 15,000 operations a day and full API functionality. The next level, Standard, is a real application (Start application opens a form), and the page quotes a 10–15 business-day review.

3. Register the callback on the OAuth client

Google Auth Platform → Clients → the web client → Authorized redirect URIs → add:

https://<api-host>/integrations/google-ads/callback

It sits alongside the sign-in, GA4/GSC and YouTube callbacks. For a local run, add http://localhost:4000/integrations/google-ads/callback as well. Google says a new URI can take from five minutes to a few hours to apply. On 2026-09-22 it worked within minutes.

A missing URI shows up as Google's redirect_uri_mismatch page, not as an error in our logs.

4. API environment

VariableValue
GOOGLE_OAUTH_CLIENT_IDthe shared web client (already set for sign-in)
GOOGLE_OAUTH_CLIENT_SECRETits secret
GOOGLE_ADS_OAUTH_REDIRECT_URIexactly the URI from step 3
GOOGLE_ADS_API_VERSIONoptional; defaults to v25

Keep the API version current. Google sunsets a major version about a year after release, and a sunset version answers every call with an HTML 404 rather than a JSON error. v21, the original default, was already gone on 2026-09-22 (v22 to v26 answered). To check a version without credentials:

sh
curl -s -o /dev/null -w "%{http_code}\n" \
  https://googleads.googleapis.com/v25/customers:listAccessibleCustomers
# 401 = the version exists; 404 = it is sunset

Bumping it is a config change. Before bumping, check the release notes for renamed fields: campaign.start_date / end_date, for example, became start_date_time / end_date_time.

5. Connect an account

Settings → Integrations → Google Ads → Connect. Google asks for the adwords scope, which covers both read and write because Google does not split it. It is a separate grant from the GA4/GSC card's.

After consent the card lists every customer the login can reach. Managers (MCC accounts) are listed but cannot be picked. Pick one, and the historical backfill starts immediately. The card reads synced once it finishes.

Production was connected on 2026-09-23: the KeyesCode customer (7960730539, USD, America/New_York, one Performance Max campaign) finished its historical sync (historicalSyncCompleted), and on 2026-09-24 it read syncStatus = synced, with hourly incrementals running.

The adwords scope and OAuth verification

adwords is a sensitive scope. Until 2026-09-24 it was missing from the consent screen's Data Access list, which the verified scopes (analytics.readonly, youtube.readonly, youtube.upload) came from. The owner's own connect works regardless. Every other Google user sees the unverified-app screen, and the project is held to the 100-user cap for unverified scopes.

On 2026-09-24 adwords was added under Google Auth Platform → Data Access and a justification saved, so the Verification Center now reads "data access is not verified". Submitting needs a demo video, the only missing field. The owner has to record it by hand: agent-recorded review videos get rejected (see the TikTok audit). The video must show:

  • the OAuth client ID in the address bar;
  • the real consent screen listing the Google Ads permission;
  • the customer picker;
  • synced campaigns in the app;
  • one approved write (for example pausing and re-enabling a campaign), and the same change shown in the Google Ads UI.

Then paste the unlisted YouTube link under Data Access → Demo video and submit from Verification Center → Prepare for verification. That page also carries an old "logo does not uniquely identify your brand" result with an optional appeal box. Branding is still verified.

What the first real run found

The code was written from Google's docs and had never touched the API before 2026-09-22. The first real run found five defects, all fixed in the same change:

  • The pinned version was sunset. The default was v21, which answered 404.
  • pageSize was removed in v19. Every page is now a fixed 10,000 rows, and single-row reads use GAQL LIMIT 1.
  • campaign.start_date / end_date do not exist in v25. The sync now reads start_date_time / end_date_time, and campaign creation sends them.
  • Campaign creation must declare EU political advertising. containsEuPoliticalAdvertising is now sent on every create.
  • Performance Max was invisible. PMax serves from asset groups, has no ad_group_ad rows, and so had no metrics. Those campaigns are now also read at campaign level.

One scheduling defect also turned up. The sync job used a fixed BullMQ jobId, and because a completed job is retained, every later "Sync now" and hourly tick was silently dropped. The job now uses BullMQ deduplication, which holds only while a job is waiting or running.


Something wrong or missing on this page? Tell us and we will fix it.