Google Ads API:环境搭建、认证、读取与写入操作
一篇技术实践指南,涵盖 Google Ads 程序化访问的完整搭建流程——从架构梳理到首次 API 调用。涉及 MCC 配置、OAuth 2.0、开发者令牌等级、google-ads Python 库、GAQL 读取查询以及 validate_only 写入模式。
架构
在成功调用 API 之前,必须准备好三个独立实体:
| 实体 | 用途 | 示例 |
|---|---|---|
| **经理账号 (MCC)** | 颁发开发者令牌 | `login_customer_id` |
| **Google Cloud 项目** | OAuth 2.0 客户端,用于认证 | Client ID + Secret |
| **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 对生产账号写入时的错误码是 RESOURCE_NOT_FOUND——而不是 PERMISSION_DENIED。这容易引起误解,因为资源确实存在且可读。该错误表示 API 在当前访问级别下拒绝路由该变更(mutation)。
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 中,进入相关项目:
- 启用 Google Ads API(API 和服务 → 库)
- 配置 OAuth 同意屏幕:
- 用户类型:内部(仅 Google Workspace 内的用户可以授权——可跳过外部应用验证)
- 填写应用名称、支持邮箱、开发者联系邮箱
- 在 数据访问 下添加范围:
https://www.googleapis.com/auth/adwords - 创建 OAuth 客户端 ID:
- 应用类型:桌面应用
- 下载客户端密钥 JSON 文件
桌面应用类型使用 http://localhost 作为重定向 URI。这是正确的做法——OAuth 流程会打开浏览器,Google 将授权码重定向到 localhost,由本地 HTTP 服务器接收。
生成 Refresh Token
以下 Python 脚本会在 127.0.0.1:0(随机端口)上运行本地 HTTP 服务器,打印授权 URL,等待一次请求,提取 code 参数,并兑换 tokens:
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'——返回 refresh token,而不仅仅是 access tokenprompt='consent'——即使用户此前已授权,也强制弹出新的同意页面;若 scope 有变更,必须使用此参数才能获取新的 refresh token
Refresh token 永久有效,除非被手动撤销、用户更改密码,或 6 个月内未使用。
已有客户端的 Scope 陷阱
若 OAuth 客户端此前已授权过其他 scope(如 Gmail、Drive、Calendar),Google 会返回旧 scope 与新 scope 的并集。google-auth-oauthlib 库默认会拒绝此 scope 不匹配的情况。在加载 flow 之前设置环境变量:
import os
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
另一种做法是从授权 URL 中省略 include_granted_scopes=true,告诉 Google 仅请求显式传入的 scope。
步骤 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(仅 owner 可读写) - 绝不提交到版本控制
- 存放在任何 Git 仓库之外
- 团队使用场景下,通过密钥管理器分发,而非共享文件
login_customer_id 是 MCC 的数字 ID,不含破折号。它告知 API 使用哪个经理账号的开发者令牌。目标 customer_id 在调用时传入,而非写在配置中。
use_proto_plus: true 启用 protobuf-plus 接口,当前 API 版本必须开启。
步骤 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 版本。截至 2026 年 6 月,google-ads 库内置了多个并行安装的版本(v19 至 v22)。始终显式指定版本——默认版本可能滞后或不支持较新字段。
验证连通性:
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` 进行写入操作
v22 API 要求传入请求对象,而非关键字参数。每个变更服务(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)
与旧版 API 的关键差异:
validate_only是请求对象上的字段,而非 mutate 方法的关键字参数- 操作通过
req.operations.append(op)追加,而非req.operations.add(op) - 资源路径为显式字符串(
'customers/1305475941/campaigns/22479990461'),而非辅助方法的返回值
三阶段变更门控
为保障生产安全,变更需经过三道门控:
第一道——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` | Test Access 令牌对生产账号操作(预期行为),或资源确实不存在 |
| `REQUIRED` | 缺少必填字段(例如广告系列创建时未指定出价策略) |
| `UNRECOGNIZED_FIELD` | 字段来自不同 API 版本,或字段名错误 |
| `INVALID_ARGUMENT` | 字段值未通过校验(例如预算为负数) |
| `PERMISSION_DENIED` | OAuth 用户无权访问目标账号 |
API 访问等级
Google Ads API 访问采用分级制,而非简单的二元开关:
| 等级 | 发放方式 | 读取(生产) | 写入(生产) | 写入(测试账号) | 每日操作上限 |
|---|---|---|---|---|---|
| **Test Access** | 从 MCC API 中心即时获取 | ✅ | ❌ | ✅ | 15,000 |
| **Basic Access** | 申请 + 合规审核 | ✅ | ✅ | ✅ | 15,000 |
| **Standard Access** | 达到花费门槛 + 审核 | ✅ | ✅ | ✅ | 无限制 |
Test Access 足以支撑开发工作:认证、GAQL 查询、validate_only 测试,以及对 Google Ads 测试账号的完整读写。Basic Access 的升级门槛是合规审核,而非广告花费。
Standard Access 解除每日操作上限,要求通过该令牌管理的花费达到一定量级,或另行提交申请。
测试账号
测试账号是免费的 Google Ads 账号,无结算信息,可接受任何变更操作。创建方法:在 MCC 中前往 账号 → + → 创建新账号 → 测试账号。将其关联至 MCC,在 Test Access 开发阶段将其作为目标 customer_id 使用。
不同访问等级下的 `validate_only`
validate_only=True 在所有访问等级下均可使用,但 Test Access 仍会以 RESOURCE_NOT_FOUND 拒绝针对生产账号的 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 年 6 月,v19 至 v22 均可使用。每个版本会新增、弃用或移除 GAQL 字段和服务方法。
版本选择规则:
- 在
load_from_storage(version='v22')中显式指定版本。默认版本可能已过时。 - 升级前查阅 Google Ads API 发布说明 了解字段弃用情况。
search_stream方法自 v6+ 起可用,是推荐的读取方式。- v12+ 版本必须使用 protobuf-plus(
use_proto_plus: true)。 - 变更操作的请求对象模式(
mutate_campaigns(request=req))在 v17+ 中替代了关键字参数风格。
本地安全
所有凭据文件必须位于版本控制之外:
~/.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` 或出价策略 |
| 创建搜索广告组 (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 会静默忽略——字段不会被更新,也不会返回错误。
完整测试脚本
一个同时读取所有实体类型并运行 validate_only 写入测试的单文件脚本:
# 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.')
完整可执行版本见广告运营仓库中的 Google Ads API 操作脚本。
参考资料
- Google Ads API 文档
- google-ads Python 库
- GAQL 参考
- 桌面应用 OAuth 2.0
- 构建项目专属的 Telegram AI 助手 — 相同的 OAuth 模式应用于不同的 Google scope
- 基于线程的 AI 运营层参考架构 — API 自动化接入的运营模型
- 小团队的规模化独自运营 — 为什么 API 自动化在规模化场景下替代了手动 UI 操作