tva
← Insights

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 中,进入相关项目:

  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 服务器接收。

生成 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 token
  • prompt='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 库内置了多个并行安装的版本(v19v22)。始终显式指定版本——默认版本可能滞后或不支持较新字段。

验证连通性:

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 要求传入请求对象,而非关键字参数。每个变更服务(CampaignServiceAdGroupServiceAdGroupCriterionServiceCampaignBudgetService)都遵循相同的模式:

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、状态 ENABLEDinclude_in_conversions_metric: true),但这仅能证明操作对象存在。它不能证明网站确实发出了相应的 AW-.../label 事件、Google 标签已加载,或已收到转化信号。

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

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

API 版本管理

google-ads Python 库内置了多个 API 版本。截至 2026 年 6 月,v19v22 均可使用。每个版本会新增、弃用或移除 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 操作脚本

参考资料

相关文章