tva
← 洞察

Google Ads API:环境搭建、认证、读取与写入操作

一篇技术实践指南,涵盖 Google Ads 程序化访问的完整搭建流程——从架构梳理到首次 API 调用。涉及 MCC 配置、OAuth 2.0、开发者令牌等级、google-ads Python 库、GAQL 读取查询以及 validate_only 写入模式。

更新(2026 年 10 月):Google 已于 2026 年 9 月 9 日停用开发者令牌。API 访问等级现按 Google Cloud 项目分配,并在 Cloud Console 的 Google Ads API Overview 页面管理;developer-token 请求头为可选项,且会被忽略;API Center 不再处理注册或访问权限变更。Google 表示现有代码仍可继续运行。下文关于开发者令牌和 API Center 的步骤描述的是 2026 年 6 月发布时的设置流程。当前的设置方法见我们的后续文章2026 年 Google Ads API 访问:Cloud 项目取代开发者令牌。

架构

在成功调用 API 之前,必须准备好三个独立实体:

实体用途示例
经理账户(MCC)颁发开发者令牌login_customer_id
Google Cloud 项目OAuth 2.0 客户端,用于认证客户端 ID + 客户端密钥
Google Ads 账户所有 API 操作的目标customer_id

三者并非同一个账户。MCC 可以持有开发者令牌,但不一定拥有目标账户——只需作为经理账户关联即可。Google Cloud 项目可以使用与 MCC 不同的 Google 身份。目标账户是广告系列(campaign)实际所在的广告主账户。

这种分离至关重要,因为 API 中心(用于发放开发者令牌)仅在经理账户中可用。独立的 Google Ads 账户即使有管理员用户,也无法访问 API 中心。

API 调用头部包含这三方身份信息,外加一个 OAuth Bearer 令牌:

developer-token: <来自 MCC API 中心>
login-customer-id: <MCC 数字 ID,不含破折号>
customer-id: <目标账号数字 ID>
Authorization: Bearer <OAuth 2.0 access token>

前置条件

  • 一个已完成设置流程的 Google Ads 经理账户
  • 目标广告账户已关联到经理账户,且关联已被接受
  • 一个已启用 Google Ads API 的 Google Cloud 项目
  • Python 3.9+,已安装 google-ads 库(pip install google-ads)
  • 一个用于一次性 OAuth 授权流程的浏览器

步骤 1:经理账户与开发者令牌

API 中心位于经理账户内的 工具与设置 → 设置 → API 中心。如果看不到 API 中心,说明当前账户不是经理账户,或者经理账户的设置尚未完成。

在 API 中心中申请开发者令牌。如果令牌仅用于您自己的账户,账户类型选择 Advertiser(而非 Agency 或 Third-Party)。令牌会立即以 Test Access 级别发放。

Test Access 允许:

  • 读取操作仅限测试账户
  • 仅对 测试账户 执行写入操作
  • 不能对生产账户执行读取或写入操作

使用 Test Access 向生产账户发送请求时,会返回 DEVELOPER_TOKEN_NOT_APPROVED(“The developer token is only approved for use with test accounts”,即开发者令牌仅获准用于测试账户)。自 2026 年 9 月开发者令牌停用后,当前 API 版本改为返回 CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION(v24 及更早版本返回 ACTION_NOT_PERMITTED)。

Basic Access 申请

要访问生产账户,需在同一 API 中心页面申请 Basic Access。申请表包含 12 个问题。对于仅在自己账户上内部使用的情况,相关回答如下:

问题回答
Q4:是否有 Google 代表对接否
Q6:商业模式描述您自己的使用场景(电商、潜在客户开发、内部报表)
Q8:哪些人将拥有访问权限内部用户——仅限员工
Q9:是否为第三方开发工具否(自有脚本/智能体)
Q10:转化跟踪与再营销 API否(除非确实在使用)
Q11:广告系列类型搜索、Performance Max、购物(根据实际情况)
Q12:功能范围广告系列创建、广告系列管理、报表

Q7 需要提交一份设计文档(PDF)。文档应描述 API 架构、认证流程、速率限制策略、错误处理以及仅限内部使用的场景说明。保持内容如实即可——合规团队会对照所声明的使用场景审核一致性。

截至 2026 年 6 月,审核周期约为 3 个工作日,并提示当前申请量高于往常。

步骤 2:OAuth 2.0

Google Cloud 项目设置

在 Google Cloud Console 中,进入相关项目:

  1. 启用 Google Ads API(API 和服务 → 库)
  2. 配置 OAuth 授权同意页面:
  • 用户类型:内部(仅 Google Workspace 内的用户可以授权——可跳过外部应用验证)
  • 填写应用名称、支持邮箱、开发者联系邮箱
  1. 在 数据访问 下添加范围:https://www.googleapis.com/auth/adwords
  2. 创建 OAuth 客户端 ID:
  • 应用类型:桌面应用
  • 下载客户端密钥 JSON 文件

桌面应用类型使用 http://localhost 作为重定向 URI。这是正确的做法——OAuth 流程会打开浏览器,Google 将授权码重定向到 localhost,由本地 HTTP 服务器接收。

生成刷新令牌

以下最小化 Python 脚本(不校验返回的 state 参数,也不设置令牌文件的访问权限;除一次性本地运行外,使用前需补上这两项措施)会在 127.0.0.1:0(随机端口)上运行本地 HTTP 服务器,打印授权 URL,等待一次请求,提取 code 参数,并换取令牌:

from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from google_auth_oauthlib.flow import Flow

class CallbackHandler(BaseHTTPRequestHandler):
    code_value = None
    def do_GET(self):
        qs = parse_qs(urlparse(self.path).query)
        CallbackHandler.code_value = (qs.get('code') or [None])[0]
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'OAuth complete.')
    def log_message(self, format, *args):
        return

server = HTTPServer(('127.0.0.1', 0), CallbackHandler)
redirect_uri = f'http://127.0.0.1:{server.server_port}/'
flow = Flow.from_client_secrets_file(
    'client_secret.json',
    scopes=['https://www.googleapis.com/auth/adwords'],
    redirect_uri=redirect_uri
)
auth_url, _ = flow.authorization_url(
    access_type='offline',
    prompt='consent'
)
print(auth_url)
server.handle_request()
flow.fetch_token(code=CallbackHandler.code_value)
Path('google_ads_token.json').write_text(flow.credentials.to_json())

两个关键参数:

  • access_type='offline'——返回 刷新令牌,而不仅仅是 访问令牌
  • prompt='consent'——即使用户此前已授权,也强制弹出新的同意页面;若授权范围有变更,必须使用此参数才能获取新的 刷新令牌

刷新令牌没有固定有效期,但在以下情况下会失效:用户撤销访问权限、令牌连续六个月未使用、用户更改密码(仅影响包含 Gmail 授权范围的令牌)、账户超出 Google 对有效刷新令牌数量的限制,或 Workspace 管理员限制授权范围或强制规定会话时长。请预先考虑重新授权。

已有客户端的授权范围陷阱

若授权 URL 设置了 include_granted_scopes=true,且用户此前已向此 OAuth 客户端授予其他授权范围(如 Gmail、Drive、Calendar),Google 会返回旧授权范围与新授权范围 的并集。google-auth-oauthlib 库默认会拒绝此授权范围不匹配的情况。在加载 flow 之前设置环境变量:

import os
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'

另一种做法是从授权 URL 中省略 include_granted_scopes=true(上述脚本未设置此参数),这样令牌就仅涵盖显式请求的授权范围。

步骤 3:google-ads.yaml

该库从 YAML 文件中读取配置:

developer_token: <value>
client_id: <value>
client_secret: <value>
refresh_token: <value>
login_customer_id: 1911599764
use_proto_plus: true

安全规则:

  • 文件权限:0600(仅文件所有者可读写)
  • 绝不提交到版本控制
  • 存放在任何 Git 仓库之外
  • 团队使用场景下,通过密钥管理器分发,而非共享文件

login_customer_id 是 MCC 的数字 ID,不含破折号。它告知 API,已通过身份验证的用户经由哪个经理账户访问目标账户;只要通过经理账户访问,就必须提供此参数。目标 customer_id 在调用时传入,而非写在配置中。

use_proto_plus 是必填的配置键:true 选择本指南使用的 proto-plus 消息接口,false 选择普通 protobuf 消息,Google 建议在对性能敏感的场景中使用后者。

步骤 4:Python 客户端初始化

from google.ads.googleads.client import GoogleAdsClient

client = GoogleAdsClient.load_from_storage(
    '/path/to/google-ads.yaml',
    version='v22'
)
gs = client.get_service('GoogleAdsService')

version 参数显式指定 API 版本。google-ads 库内置了多个并行安装的版本;本文示例使用 v22。截至 2026 年 10 月,当前库版本已不再包含 v22(于 33.0.0 中移除),34.0.0 则新增了 v26,因此请替换为当前版本。始终显式指定版本——默认版本可能滞后或不支持较新字段。

验证连通性:

cs = client.get_service('CustomerService')
customers = cs.list_accessible_customers()
ids = [r.split('/')[-1] for r in customers.resource_names]
# 示例输出:['1305475941', '3977581086', '1911599764']

步骤 5:读取操作(GAQL)

Google Ads 使用 GAQL(Google Ads Query Language),一种类 SQL 的查询语法。所有读取操作均通过 GoogleAdsService.search_stream() 执行,该方法可避免分页问题,并在大多数查询中优于 search()。

广告系列及其效果指标

SELECT
  campaign.id,
  campaign.name,
  campaign.status,
  campaign.advertising_channel_type,
  campaign.serving_status,
  campaign.bidding_strategy_type,
  metrics.impressions,
  metrics.clicks,
  metrics.cost_micros,
  metrics.conversions,
  metrics.conversions_value,
  metrics.ctr,
  metrics.average_cpc
FROM campaign
ORDER BY campaign.id

广告系列预算

SELECT
  campaign_budget.id,
  campaign_budget.name,
  campaign_budget.amount_micros,
  campaign_budget.delivery_method,
  campaign_budget.status,
  campaign.id,
  campaign.name
FROM campaign_budget

广告组

SELECT
  ad_group.id,
  ad_group.name,
  ad_group.status,
  ad_group.cpc_bid_micros,
  campaign.id,
  campaign.name
FROM ad_group
ORDER BY campaign.id, ad_group.id

关键词(按展示次数排序)

SELECT
  ad_group_criterion.keyword.text,
  ad_group_criterion.keyword.match_type,
  ad_group_criterion.status,
  ad_group_criterion.criterion_id,
  ad_group.id,
  ad_group.name,
  campaign.id,
  campaign.name,
  metrics.impressions,
  metrics.clicks,
  metrics.cost_micros,
  metrics.conversions
FROM keyword_view
WHERE ad_group_criterion.type = 'KEYWORD'
ORDER BY metrics.impressions DESC
LIMIT 100

广告及其政策状态

SELECT
  ad_group_ad.ad.id,
  ad_group_ad.ad.name,
  ad_group_ad.ad.type,
  ad_group_ad.status,
  ad_group_ad.policy_summary.approval_status,
  ad_group.id,
  ad_group.name,
  campaign.id,
  campaign.name
FROM ad_group_ad

转化操作

SELECT
  conversion_action.id,
  conversion_action.name,
  conversion_action.status,
  conversion_action.type,
  conversion_action.category,
  conversion_action.include_in_conversions_metric,
  conversion_action.counting_type
FROM conversion_action

Python 执行模式

def gaql_query(client, customer_id, query):
    gs = client.get_service('GoogleAdsService')
    results = []
    for batch in gs.search_stream(customer_id=customer_id, query=query):
        for row in batch.results:
            results.append(row)
    return results

步骤 6:使用 validate_only 进行写入操作

下方示例使用请求对象而非关键字参数,因为请求对象承载 validate_only。每个变更服务(CampaignService、AdGroupService、AdGroupCriterionService、CampaignBudgetService)都遵循相同的模式:

from google.protobuf import field_mask_pb2

req = client.get_type('MutateCampaignsRequest')
req.customer_id = '1305475941'
req.validate_only = True
op = client.get_type('CampaignOperation')
op.create.name = 'Campaign Name'
op.create.status = client.enums.CampaignStatusEnum.PAUSED
op.create.advertising_channel_type = client.enums.AdvertisingChannelTypeEnum.SEARCH
op.create.campaign_budget = 'customers/1305475941/campaignBudgets/123456789'
op.create.manual_cpc.enhanced_cpc_enabled = False
req.operations.append(op)  # 使用 .append(),而非 .add()
client.get_service('CampaignService').mutate_campaigns(request=req)

此模式的要点(使用 use_proto_plus: true):

  • validate_only 是请求对象上的字段,而非 mutate 方法的关键字参数
  • 操作通过 req.operations.append(op) 追加;.add() 属于普通 protobuf 接口(use_proto_plus: false)
  • 本文将资源路径写为显式字符串('customers/1305475941/campaigns/22479990461');服务也提供路径辅助方法,例如 campaign_service.campaign_path(customer_id, campaign_id),Google 建议使用这些方法,而非手动拼接资源名称

三阶段变更门控

为保障生产安全,变更需经过三道门控:

第一道——validate_only=True: API 校验请求结构、必填字段和资源引用。不会创建任何对象。每次变更前先执行此步骤。

第二道——暂停创建: 将 validate_only=False,以 status=PAUSED 创建对象。继续操作前,通过读取查询验证结果。

第三道——启用上线: 单独发起一次 API 调用,仅将 status 字段更新为 ENABLED。此操作需要显式批准,不应与创建操作合并。

# 第三道门控:启用之前创建的暂停广告系列
req = client.get_type('MutateCampaignsRequest')
req.customer_id = '1305475941'
req.validate_only = False
op = client.get_type('CampaignOperation')
op.update.resource_name = 'customers/1305475941/campaigns/22479990461'
op.update.status = client.enums.CampaignStatusEnum.ENABLED
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['status']))
req.operations.append(op)
client.get_service('CampaignService').mutate_campaigns(request=req)

更新预算

req = client.get_type('MutateCampaignBudgetsRequest')
req.customer_id = '1305475941'
req.validate_only = True
op = client.get_type('CampaignBudgetOperation')
op.update.resource_name = 'customers/1305475941/campaignBudgets/123456789'
op.update.amount_micros = 1_000_000  # 每日 $1.00
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['amount_micros']))
req.operations.append(op)
client.get_service('CampaignBudgetService').mutate_campaign_budgets(request=req)

更新广告组出价

req = client.get_type('MutateAdGroupsRequest')
req.customer_id = '1305475941'
req.validate_only = True
op = client.get_type('AdGroupOperation')
op.update.resource_name = 'customers/1305475941/adGroups/165827073530'
op.update.cpc_bid_micros = 250_000  # $0.25
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['cpc_bid_micros']))
req.operations.append(op)
client.get_service('AdGroupService').mutate_ad_groups(request=req)

创建关键词

req = client.get_type('MutateAdGroupCriteriaRequest')
req.customer_id = '1305475941'
req.validate_only = True
op = client.get_type('AdGroupCriterionOperation')
op.create.ad_group = 'customers/1305475941/adGroups/165827073530'
op.create.status = client.enums.AdGroupCriterionStatusEnum.PAUSED
op.create.keyword.text = 'example keyword'
op.create.keyword.match_type = client.enums.KeywordMatchTypeEnum.EXACT
req.operations.append(op)
client.get_service('AdGroupCriterionService').mutate_ad_group_criteria(request=req)

错误处理

from google.ads.googleads.errors import GoogleAdsException

try:
    response = service.mutate_campaigns(request=req)
except GoogleAdsException as ex:
    for error in ex.failure.errors:
        print(f'{error.error_code}: {error.message}')

常见错误码及其含义:

错误码原因
RESOURCE_NOT_FOUND引用的资源不存在、已被移除,或资源名称格式错误
DEVELOPER_TOKEN_NOT_APPROVEDTest Access 令牌用于生产账户(自 2026 年 9 月停用后改为 CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION)
REQUIRED缺少必填字段(例如广告系列创建时未指定出价策略)
UNRECOGNIZED_FIELD字段来自不同 API 版本,或字段名错误
INVALID_ARGUMENT字段值未通过校验(例如预算为负数)
PERMISSION_DENIEDOAuth 用户无权访问目标账户

API 访问等级

Google Ads API 访问采用分级制,而非简单的二元开关:

等级发放方式读取(生产)写入(生产)写入(测试账户)每日操作上限
Test Access即时获取(2026 年 9 月前通过 MCC API 中心获取;现为在 Cloud 项目中启用 API 时自动获得)❌❌✅15,000
Explorer Access(截至 2026 年 10 月已列出)在 Cloud Console 的 Overview 页面申请✅✅(部分服务受限)✅生产账户 2,880 / 测试账户 15,000
Basic Access申请 + 合规审核✅✅✅15,000
Standard Access取得 Basic Access 后申请 + 人工审核✅✅✅无限制

Test Access 足以支撑针对 Google Ads 测试账户的开发工作:认证、GAQL 查询、validate_only 测试,以及完整读写。Basic Access 的升级门槛是合规审核,而非广告花费。

Standard Access 解除每日操作上限(各功能的速率限制仍然适用)。它要求另行提交申请,且只有取得 Basic Access 后才能申请,还需接受人工审核;Google 的访问等级文档并未规定广告花费门槛。

测试账户

测试账户是免费的 Google Ads 账户,不产生费用,也绝不会投放广告。测试账户与生产账户不能交互,因此测试账户不能位于生产 MCC 下:请使用未关联至生产 MCC 的 Google 账户登录,创建独立的测试经理账户,再在其下创建测试客户账户。在 Test Access 开发阶段,选择其中一个测试客户账户作为目标 customer_id,并将测试经理账户设为 login_customer_id。

不同访问等级下的 validate_only

validate_only=True 在所有访问等级下均可使用,但 Test Access 仍会以与其他请求相同的授权错误拒绝针对生产账户的 validate_only 变更。这并不是 validate_only 的限制——无论 validate_only 标志如何,所有写入操作都受到相同的访问控制。

转化跟踪:API 读取与功能状态

即使转化操作已存在且已启用,Google Ads 界面仍可能显示“设置转化跟踪”的建议。API 可以确认转化操作的存在(类型 WEBPAGE、类别 PURCHASE、状态 ENABLED、include_in_conversions_metric: true),但这仅能证明操作对象存在。它不能证明网站确实发出了相应的 AW-.../label 事件、Google 标签已加载,或已收到转化信号。

在审计转化跟踪时,应将 API 检查(转化操作元数据)与浏览器检查(网络标签页、标签加载、事件触发)分开。API 是配置的事实来源;浏览器是执行的事实来源。

后端收入归因 的模式在此适用:如果客户端转化事件不够可靠,应规划通过 Google Ads API 的 ConversionUploadService 进行服务端或离线转化上传作为备用方案。

API 版本管理

google-ads Python 库内置了多个 API 版本。截至 2026 年 10 月,最新版本(34.0.0)新增了 v26,而 v22 及更早版本已从库中移除。每个版本会新增、弃用或移除 GAQL 字段和服务方法。

版本选择规则:

  • 在 load_from_storage(version='v22') 中显式指定版本。默认版本可能已过时。
  • 升级前查阅 Google Ads API 发布说明 了解字段弃用情况。
  • search_stream 方法在所有当前版本中均可用,是推荐的读取方式。
  • 必须显式设置 use_proto_plus(true 或 false);它选择的是 Python 消息接口,而非 API 版本。
  • 变更操作的请求对象模式(mutate_campaigns(request=req))承载请求级字段,例如 validate_only;Google 的当前示例仍将 customer_id 和 operations 作为关键字参数传入普通变更调用。

本地安全

所有凭据文件必须位于版本控制之外:

~/.hermes/
  google-ads.yaml          # 0600 — 开发者令牌、OAuth 客户端、refresh token
  google_ads_token.json    # 0600 — OAuth tokens,adwords scope
  google_client_secret.json # 0600 — OAuth client ID + secret

在项目仓库中添加 .gitignore:

google-ads.yaml
*_token*.json
*_secret*.json

可提交一份包含占位值的模板文件,用于说明预期结构:

# google-ads.yaml.template — 提交此文件,本地填充实际值
developer_token: INSERT_DEV_TOKEN
client_id: INSERT_CLIENT_ID
client_secret: INSERT_CLIENT_SECRET
refresh_token: INSERT_REFRESH_TOKEN
login_customer_id: INSERT_MCC_ID
use_proto_plus: true

常见操作的必填字段

操作必填字段
创建搜索广告系列(Campaign)name、status、advertising_channel_type、campaign_budget、manual_cpc 或出价策略、contains_eu_political_advertising
创建搜索广告组(Ad Group)name、status、type_、cpc_bid_micros、campaign
创建关键词(Keyword)ad_group、status、keyword.text、keyword.match_type
更新广告系列状态resource_name、status、update_mask(paths: ['status'])
更新预算resource_name、amount_micros、update_mask(paths: ['amount_micros'])

缺少必填字段会返回 REQUIRED 并附带字段名。更新操作缺少 update_mask 会被拒绝,并返回 FieldMaskError.FIELD_MASK_MISSING。

精简测试脚本

以下精简脚本读取所有实体类型并运行 validate_only 写入测试(GAQL 字段列表和筛选条件以 ... 缩略表示,ID 也只是占位符,运行前请补全):

# google_ads_api_test.py — 读取所有实体 + validate_only 写入测试
import warnings
warnings.filterwarnings('ignore')
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
from google.protobuf import field_mask_pb2

CFG = '/path/to/google-ads.yaml'
CID = '1305475941'
c = GoogleAdsClient.load_from_storage(CFG, version='v22')
gs = c.get_service('GoogleAdsService')

def gaql(query, label):
    print(f'\n=== {label} ===')
    n = 0
    for batch in gs.search_stream(customer_id=CID, query=query):
        for row in batch.results:
            n += 1
    print(f'{n} rows')

# 读取所有实体
gaql("SELECT customer.id, customer.descriptive_name FROM customer WHERE customer.id = ...", "CUSTOMER")
gaql("SELECT campaign.id, campaign.name, campaign.status, ...  FROM campaign", "CAMPAIGNS")
gaql("SELECT campaign_budget.id, campaign_budget.name, ...  FROM campaign_budget", "BUDGETS")
gaql("SELECT ad_group.id, ad_group.name, ...  FROM ad_group", "AD GROUPS")
gaql("SELECT ad_group_criterion.keyword.text, ...  FROM keyword_view WHERE ...", "KEYWORDS")
gaql("SELECT ad_group_ad.ad.id, ...  FROM ad_group_ad", "ADS")
gaql("SELECT conversion_action.id, ...  FROM conversion_action", "CONVERSIONS")

# 写入测试
def test_write(name, fn):
    print(f'\n--- {name} ---')
    try:
        fn()
        print('PASS')
    except GoogleAdsException as e:
        for err in e.failure.errors:
            print(f'{err.error_code}: {err.message}')

def test_create_campaign():
    req = c.get_type('MutateCampaignsRequest')
    req.customer_id = CID
    req.validate_only = True
    op = c.get_type('CampaignOperation')
    op.create.name = 'TEST_CAMPAIGN'
    op.create.status = c.enums.CampaignStatusEnum.PAUSED
    op.create.advertising_channel_type = c.enums.AdvertisingChannelTypeEnum.SEARCH
    op.create.campaign_budget = f'customers/{CID}/campaignBudgets/123456789'
    op.create.manual_cpc.enhanced_cpc_enabled = False
    req.operations.append(op)
    c.get_service('CampaignService').mutate_campaigns(request=req)

test_write('Create Campaign (validate_only)', test_create_campaign)
print('\nDone.')

参考资料

延伸阅读