10+ years · 3x UK Search Awards

Blog

Google Ads API Authentication: OAuth, Service Accounts & Developer Token (2026)

Blog |Automation|2026-08-29|11 min

Automation · 2026-08-29 · 11 min

In short

Google Ads API authentication has three layers that get mixed up constantly — the OAuth2 credentials that say who you are, the developer token that says which application you are, and the login-customer-id that says which account you're targeting. A fourth, optional layer (service accounts) exists for a narrower use case than the quick-start docs let on. Get any one of the three wrong and you don't get a warning — you get a specific error code and a dead script.

4

values in google-ads.yaml

7

days before a Testing-mode refresh token dies

4

recurring auth errors mapped to fixes below

$0

cost of setting any of this up correctly

Quick answer

A Google Ads API developer token identifies your application and never changes; an OAuth2 client (client ID and secret) identifies your app to Google's auth servers; a refresh token identifies the person who granted access and can expire. For a solo operator or agency running your own scripts, use the OAuth2 desktop (installed app) flow — it's the simplest path and offline access is on by default. Use the web application flow only if you're building something users log into through a browser. Service accounts make sense only for server-to-server automation inside a Google Workspace domain with domain-wide delegation configured for the adwords scope — Google's own service account guide doesn't spell this requirement out, but skip it and the call fails with AuthenticationError.NOT_ADS_USER.

My Basic Access application for the Google Ads API got reviewed in a few hours instead of the usual days — I wrote about the exact steps in the Basic Access guide. But an approved token doesn't mean anything until you get through the auth handshake, and that's a separate problem entirely — the first time I hit it hard, a Google Merchant Center token died mid-script with no warning at all.

This guide is the part nobody explains cleanly: OAuth2 desktop vs. web flow, the 7-day trap, what a developer token header actually is versus an OAuth token, when a service account is genuinely the right tool (and when it silently isn't), and a two-minute Python test to confirm the whole chain works before you build anything on top of it.


The three (plus one) layers of Google Ads API auth

The single most common beginner mix-up: a developer token is not an OAuth token, and getting one approved doesn't authenticate anything by itself. They're two unrelated credentials that both have to be present on every call, plus a third that only matters once you're working through a manager account.

LayerWhat it isWhere it livesIf it's wrong or missing
OAuth2 client (client ID + secret)Identifies your application to Google's auth serversCloud Console → Credentials → Create OAuth client IDThe auth flow can't start
Refresh tokenA long-lived pass that's exchanged for short-lived access tokensOutput of the OAuth consent flow (desktop or web)invalid_grant — the script stops
Developer tokenIdentifies your application to the Google Ads API — not the userAPI Center, in your manager accountDEVELOPER_TOKEN_NOT_APPROVED
login-customer-idSays which account (MCC or client) you're targetingA header you set, required when going through an MCCUSER_PERMISSION_DENIED

The developer token itself — how it's generated, and the Test / Explorer / Basic / Standard access levels that gate what it can call — is covered in full in the Basic Access guide. This post assumes you have a token (any level works for testing) and focuses on getting the other layers right.


OAuth2: desktop flow vs. web flow

The Google Ads API supports two OAuth2 flows, and picking the right one saves you a class of bugs later. Per Google's own OAuth internals documentation, the desktop (installed app) flow has offline access — the ability to refresh a token without the user sitting there — turned on by default, so you don't have to explicitly request it. The web application flow doesn't; it needs an explicit access_type=offline parameter on the auth request, or your refresh token never shows up.

For a solo operator or an agency running internal scripts — reporting pulls, budget-pacing checks, bid changes — the desktop flow is the right default. It's what I run for every client account under my own manager account. The web flow is for the other case: an app with a login screen that a third party uses.

1
Open the OAuth consent screen — same Cloud Console project you'd use for the developer token. Configure it once (app name, support email, scopes) if you haven't already.
2
Credentials → Create OAuth client ID — pick application type Desktop app, not Web application, unless you specifically need the browser-login flow.
3
Download the client secret JSON — this holds the client ID and client secret your code will read. Never commit it to a repository.
4
Run the local auth flow once InstalledAppFlow.run_local_server() in Python opens a browser, you approve access with your Google account, and the flow hands you a refresh token in the terminal.
5
Save the refresh token into google-ads.yaml — alongside the client ID, client secret, and developer token. You only run steps 1-4 once; after that, the library refreshes access tokens on its own.

If you've already been through the OAuth consent screen for brand verification — Step 4 in the Basic Access guide — you'll recognize this screen. It's the same one; you're just here for a different reason this time.


The trap: Testing mode kills your refresh token in 7 days

This one is documented, but easy to miss. Per Google's own OAuth 2.0 documentation, a Cloud project with an External user type and a Testing publishing status issues refresh tokens that expire after 7 days — no email, no warning banner, nothing. The next call just fails with invalid_grant ("Token has been expired or revoked"), and if the script runs unattended overnight, you find out when the report doesn't show up. I've walked through exactly how this cost me a dead Google Merchant Center token, and the fix, in the Google API access guide for agencies. If your project is the same one you already pushed to In production for brand verification (Step 4 of the Basic Access guide), you've already fixed this — nothing more to do. If not, check now: Cloud Console → APIs and services → OAuth consent screen → Audience tab.


Developer token header + login-customer-id

Two more pieces get confused for each other constantly, and neither is an OAuth concept.

The developer token is not a header you get from an OAuth flow — it's a fixed string from your manager account's API Center (22 characters in my accounts), and it goes on every request as a developer-token HTTP/gRPC header. It identifies the application, not the person calling it, and it's the same value regardless of which Google account authenticated the call.

login-customer-id only matters once your authenticated account has access to a manager (MCC) account. If you're calling the API to act on a client account underneath that MCC, you have to tell Google which account context you're operating in — set the login-customer-id header to the MCC's ID. Skip it and you get USER_PERMISSION_DENIED: "the authorized customer does not have access to the operating customer," even though, from the Google Ads UI, you clearly do.


Service accounts — the catch the quick reference skips

Google's own service account guide makes it look simple: create a service account, download the JSON key, sign into Google Ads as an admin, go to Admin → Access and security, add the service account's email as a user, set json_key_file_path in your config. Four steps, done.

Why this trips people up

In practice, that guide leaves out a step. Without a Google Workspace domain and domain-wide delegation configured for the adwords scope, the call still fails — typically with AuthenticationError.NOT_ADS_USER, the same error you'd see from an OAuth account with no Google Ads access at all. Developers hitting this have documented the fix on Google's own Ads API developer forum: the service account has to impersonate a real Workspace user via a subject parameter, and that impersonation step is what actually authenticates the request, not the service account credential on its own.

When it's worth it: server-to-server automation with no human in the loop for every token refresh, and — usefully — access that doesn't break the day an employee leaves, since it isn't tied to any one person's Google account. Per Google's guide, one service account email can be added to up to 20 Google Ads accounts; beyond that, Google's guidance is to add it under a manager account instead.

When it isn't: a solo operator or a small team without a Workspace domain. That's my actual setup — every script I run against the Google Ads API uses the desktop OAuth flow above, not a service account, because it's simpler to configure and doesn't require standing up domain-wide delegation for one person's automation.


Whichever flow you use, the Python client library reads everything from one YAML file. Here's the shape of it — replace every value, obviously, this isn't a real config:

developer_token: "INSERT_DEVELOPER_TOKEN_HERE"
client_id: "INSERT_OAUTH2_CLIENT_ID_HERE"
client_secret: "INSERT_OAUTH2_CLIENT_SECRET_HERE"
refresh_token: "INSERT_REFRESH_TOKEN_HERE"
login_customer_id: "1234567890"   # MCC ID, digits only, no dashes — omit this line entirely for a standalone account
use_proto_plus: True

Two things worth flagging: login_customer_id is the MCC ID without dashes, and it only needs to be set if you're calling through a manager account — leave it out for a standalone account. And use_proto_plus: True isn't optional cosmetics; the client library requires this field in its configuration, and its absence produces confusing type errors that have nothing to do with authentication at all.


Verify it works: a minimal Python test

Before building anything on top of this, confirm the whole chain — OAuth2, developer token, login-customer-id — actually connects. The same principle as testing at Explorer access before applying for Basic: prove the plumbing works on the smallest possible call first.

# 1) One-time: generate a refresh token (desktop OAuth flow)
from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = ["https://www.googleapis.com/auth/adwords"]
flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", scopes=SCOPES)
credentials = flow.run_local_server(port=8080, prompt="consent")
print(credentials.refresh_token)  # paste this into google-ads.yaml

# 2) First call: does the connection actually work?
from google.ads.googleads.client import GoogleAdsClient

client = GoogleAdsClient.load_from_storage("google-ads.yaml")
ga_service = client.get_service("GoogleAdsService")

query = "SELECT customer.id, customer.descriptive_name FROM customer LIMIT 1"
response = ga_service.search(customer_id="9876543210", query=query)

for row in response:
    print(row.customer.descriptive_name)

If that returns an account name, every layer — OAuth2, developer token, login-customer-id — is correctly wired. If it fails, the error you get back tells you exactly which layer to fix; see the table below.


Common errors and what actually causes them

ErrorWhat it meansTypical causeFix
invalid_grant / RefreshErrorYour refresh token is deadOAuth consent screen in Testing status — refresh tokens expire after 7 daysSwitch publishing status to In production; regenerate the refresh token once
USER_PERMISSION_DENIEDYou don't have rights to this account, in this calllogin-customer-id missing when calling a client account under an MCCSet the login-customer-id header to your MCC's ID
AuthenticationError.NOT_ADS_USERThe account behind the token isn't a Google Ads user(a) OAuth account has no Google Ads access, or (b) service account missing the subject impersonation parameter(a) Authenticate with an account that has Ads access; (b) add subject/impersonated email and confirm domain-wide delegation
DEVELOPER_TOKEN_NOT_APPROVEDYour token can't use this account or this serviceTest-level token against a production account, or an Explorer-level token calling a service Explorer doesn't coverSee the Test → Explorer → Basic progression in the Basic Access guide

All four descriptions above are quoted from Google's official common-errors documentation. The causes and fixes are what I've actually run into wiring this up for client accounts.


Frequently asked questions

What's the difference between a developer token and an OAuth2 token in the Google Ads API?
A developer token identifies your application — it's a fixed string issued once in your manager account's API Center (22 characters in my accounts), and it never expires on its own. An OAuth2 access token (and the refresh token behind it) identifies the person who authorized your app, and it can expire or be revoked. Every Google Ads API call needs both: the developer token as a header, and a valid OAuth2 access token for authentication.
Why does my refresh token stop working every 7 days?
Your Google Cloud project's OAuth consent screen is set to an External user type with a Testing publishing status. Google documents this: refresh tokens issued under those conditions expire after 7 days, which surfaces as invalid_grant or a RefreshError. Switching the publishing status to In production removes that 7-day expiry — you only need to do it once per project.
Do I need Google Workspace to use a service account with the Google Ads API?
In practice, yes. Google's quick-start steps for service accounts don't spell this out, but developers who've implemented it report that without a Google Workspace domain and domain-wide delegation configured for the adwords scope, the call fails with AuthenticationError.NOT_ADS_USER — the service account has to impersonate a real Workspace user via a subject parameter to authenticate successfully. For a solo operator or small team without a Workspace domain, the OAuth2 desktop flow is simpler and doesn't require any of this.
What is login-customer-id and when do I need it?
login-customer-id is a header that tells the Google Ads API which account context you're operating in. It's required whenever the authenticated account accesses a client account through a manager (MCC) account — set it to the MCC's ID. Leaving it out when it's needed produces USER_PERMISSION_DENIED, even if you can see the account fine in the Google Ads UI.
What does AuthenticationError.NOT_ADS_USER mean?
Per Google's official documentation, it means the Google account used to generate the access token isn't associated with any Google Ads account. It shows up in two situations: an OAuth login with no Google Ads access at all, or a service account call missing the subject/impersonated-user parameter it needs to authenticate as a real Workspace user.

Want the API doing the monitoring instead of you?

I build and run this exact auth stack for client accounts — nightly reporting, budget-pacing checks, disapproval alerts — on top of a properly configured Google Ads API connection, no 3am invalid_grant surprises.

Schedule a free consultation
Last updated: August 29, 2026

Free video audit

Get a Personalised Video Audit of Your Google Ads Account

I'll record a 15-minute walkthrough of your campaigns showing exactly where you're losing money and what to fix first.

Book a free consultation

Free personalised video audit

Want a video walkthrough of your Google Ads account?

I'll personally record a 15-minute video walking through your campaigns, showing you where you're losing money and giving you 3 specific things to fix immediately. No sales pitch — just value.

What you get:

  • 15-min personalised video analysis
  • 3 specific quick wins to implement
  • Budget & bidding recommendations

Requirements:

  • Ad spend: €1,500+/month (or £1,500+)
  • Active account for 3+ months
  • eCommerce or Lead Gen business

Limited to 5 audits per month. Response within 48 hours.