Google Ads API: Configuração, Autenticação, Operações de Leitura e Escrita
Um guia técnico para configurar o acesso programático ao Google Ads — da arquitetura à primeira chamada de API. Abrange configuração de MCC, OAuth 2.0, níveis de Developer Token, a biblioteca Python google-ads, consultas de leitura em GAQL e o padrão de escrita validate_only.
Arquitetura
São necessárias três entidades separadas antes que uma única chamada de API funcione:
| Entidade | Finalidade | Exemplo |
|---|---|---|
| **Conta de Gestor (MCC)** | Emite o Developer Token | `login_customer_id` |
| **Projeto Google Cloud** | Cliente OAuth 2.0 para autenticação | Client ID + Secret |
| **Conta Google Ads** | Alvo de todas as operações da API | `customer_id` |
Não são a mesma conta. Uma MCC pode possuir um Developer Token sem ser proprietária da conta de destino — apenas precisa estar vinculada como gestora. O Projeto Google Cloud pode estar sob uma identidade Google diferente da MCC. A conta de destino é a conta de anunciante onde as campanhas residem.
Esta separação é importante porque o API Center (onde os Developer Tokens são provisionados) só está disponível dentro de Contas de Gestor. Uma conta Google Ads autónoma não pode aceder ao API Center, mesmo que tenha utilizadores administrativos.
O cabeçalho da chamada de API reúne estas três identidades mais um token OAuth Bearer:
developer-token: <do API Center da MCC>
login-customer-id: <ID numérico da MCC, sem hífenes>
customer-id: <ID numérico da conta de destino>
Authorization: Bearer <token de acesso OAuth 2.0>
Pré-requisitos
- Uma Conta de Gestor Google Ads com configuração concluída
- A conta de anúncios de destino vinculada à Conta de Gestor e o vínculo aceite
- Um Projeto Google Cloud com a API Google Ads ativada
- Python 3.9+ com a biblioteca
google-ads(pip install google-ads) - Um navegador para o fluxo único de consentimento OAuth
Passo 1: Conta de Gestor e Developer Token
O API Center encontra-se em Ferramentas e Definições → Configuração → API Center dentro de uma Conta de Gestor. Se o API Center estiver ausente, a conta ou não é uma Conta de Gestor ou a sua configuração está incompleta.
A partir do API Center, solicite um Developer Token. Defina o tipo de conta como Advertiser (não Agency ou Third-Party) quando o token se destina às suas próprias contas. O token é emitido imediatamente com o nível Test Access.
O Test Access permite:
O erro para escritas em contas de produção com Test Access é RESOURCE_NOT_FOUND — e não PERMISSION_DENIED. Isto é confuso porque o recurso existe e é legível. O erro significa que a API se recusa a encaminhar a mutação sob o nível de acesso atual.
Candidatura a Basic Access
Para escrever em contas de produção, candidate-se a Basic Access a partir da mesma página do API Center. O formulário de candidatura coloca 12 perguntas. Para uso interno nas suas próprias contas, as respostas relevantes são:
| Pergunta | Resposta |
|---|---|
| Q4: Relação com um Representante Google | No |
| Q6: Modelo de Negócio | Descreva o seu caso de uso (e-commerce, geração de leads, reporting interno) |
| Q8: Quem terá acesso | Utilizadores internos — apenas colaboradores |
| Q9: Ferramenta desenvolvida por terceiros | No (scripts/agentes próprios) |
| Q10: API de Acompanhamento de Conversões e Remarketing | No (a menos que realmente a utilize) |
| Q11: Tipos de Campanha | Search, Performance Max, Shopping (conforme aplicável) |
| Q12: Capacidades | Criação de Campanhas, Gestão de Campanhas, Reporting |
É necessário um documento de design (PDF) para a Q7. Deve descrever a arquitetura da API, o fluxo de autenticação, a estratégia de limitação de taxa, o tratamento de erros e o caso de uso exclusivamente interno. Seja factual — a equipa de compliance revê a consistência com o caso de uso declarado.
Tempo de processamento em junho de 2026: aproximadamente 3 dias úteis, com uma nota sobre volume de candidaturas superior ao habitual.
Passo 2: OAuth 2.0
Configuração do Projeto Google Cloud
Na Google Cloud Console, sob o projeto relevante:
- Ative a Google Ads API (APIs e Serviços → Biblioteca)
- Configure o ecrã de consentimento OAuth:
https://www.googleapis.com/auth/adwordsO tipo Desktop app utiliza http://localhost como URI de redirecionamento. Isto está correto — o fluxo OAuth abre um navegador, o Google redireciona para localhost com o código de autorização e um servidor HTTP local captura-o.
Geração do Refresh Token
Um script Python que executa um servidor HTTP local em 127.0.0.1:0 (porta aleatória), imprime o URL de autorização, aguarda um pedido, extrai o parâmetro code e troca-o por 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())
Dois parâmetros são importantes:
access_type='offline' — devolve um refresh token, não apenas um access tokenprompt='consent' — força um novo ecrã de consentimento mesmo que o utilizador tenha autorizado anteriormente; necessário para obter um novo refresh token se os scopes tiverem mudadoO refresh token é permanente a menos que seja revogado, o utilizador altere a palavra-passe ou o token fique sem uso durante 6 meses.
Armadilha de Scope com Clientes Existentes
Se o cliente OAuth foi previamente autorizado para outros scopes (Gmail, Drive, Calendar), o Google devolve a união dos scopes antigos e novos. A biblioteca google-auth-oauthlib rejeita esta discrepância de scope por padrão. Defina a variável de ambiente antes de carregar o fluxo:
import os
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
Em alternativa, omita include_granted_scopes=true do URL de autorização, o que indica ao Google para solicitar apenas o scope explicitamente indicado.
Passo 3: `google-ads.yaml`
A biblioteca lê a configuração de um ficheiro YAML:
developer_token: <valor>
client_id: <valor>
client_secret: <valor>
refresh_token: <valor>
login_customer_id: 1911599764
use_proto_plus: true
Regras de segurança:
0600 (apenas leitura/escrita pelo proprietário)login_customer_id é o ID numérico da MCC sem hífenes. Indica à API qual o Developer Token da conta de gestor a utilizar. O customer_id de destino é passado no momento da chamada, não na configuração.
use_proto_plus: true ativa a interface protobuf-plus, que é necessária para as versões atuais da API.
Passo 4: Inicialização do Cliente 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')
O parâmetro version seleciona explicitamente a versão da API. A biblioteca google-ads é distribuída com várias versões instaladas em paralelo (v19 a v22 em junho de 2026). Especifique sempre a versão — a padrão pode estar desatualizada ou não suportar campos mais recentes.
Verifique a conectividade:
cs = client.get_service('CustomerService')
customers = cs.list_accessible_customers()
ids = [r.split('/')[-1] for r in customers.resource_names]
# Exemplo de saída: ['1305475941', '3977581086', '1911599764']
Passo 5: Operações de Leitura (GAQL)
O Google Ads utiliza GAQL (Google Ads Query Language), uma sintaxe semelhante a SQL. Todas as operações de leitura passam por GoogleAdsService.search_stream(), que evita paginação e é preferível a search() para a maioria das consultas.
Campanhas com Métricas de Performance
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
Orçamentos de Campanha
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
Grupos de Anúncios
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
Palavras-chave (por Impressões)
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
Anúncios com Estado de Política
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
Ações de Conversão
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
Padrão de Execução 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
Passo 6: Operações de Escrita com `validate_only`
A API v22 exige objetos de pedido, não argumentos nomeados. Todos os serviços de mutação (CampaignService, AdGroupService, AdGroupCriterionService, CampaignBudgetService) seguem o mesmo padrão:
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(), não .add()
client.get_service('CampaignService').mutate_campaigns(request=req)
Diferenças-chave em relação a versões anteriores da API:
validate_only é um campo no objeto de pedido, não um argumento nomeado do método mutatereq.operations.append(op), e não req.operations.add(op)'customers/1305475941/campaigns/22479990461'), não valores devolvidos por métodos auxiliaresPortão de Mutação em Três Fases
Para segurança em produção, as mutações passam por três portões:
Portão 1 — validate_only=True: A API valida a estrutura do pedido, campos obrigatórios e referências de recursos. Nenhum objeto é criado. Execute isto primeiro para cada mutação.
Portão 2 — Criação em Pausa: Defina validate_only=False e crie o objeto com status=PAUSED. Verifique o resultado com uma consulta de leitura antes de prosseguir.
Portão 3 — Ativação Real: Uma chamada de API separada que atualiza apenas o campo status para ENABLED. Isto deve exigir aprovação explícita, não ser incorporado na criação.
# Portão 3: ativar uma campanha previamente criada em pausa
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)
Atualização de Orçamento
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 por dia
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['amount_micros']))
req.operations.append(op)
client.get_service('CampaignBudgetService').mutate_campaign_budgets(request=req)
Atualização de Lance de Grupo de Anúncios
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)
Criação de Palavra-chave
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)
Tratamento de Erros
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}')
Códigos de erro comuns e o seu significado:
| Código de Erro | Causa |
|---|---|
| `RESOURCE_NOT_FOUND` | Token Test Access contra uma conta de produção (esperado) ou recurso genuinamente inexistente |
| `REQUIRED` | Campo obrigatório em falta (ex.: estratégia de lance na criação de campanha) |
| `UNRECOGNIZED_FIELD` | Campo de uma versão diferente da API ou nome de campo incorreto |
| `INVALID_ARGUMENT` | Valor de campo falha validação (ex.: orçamento negativo) |
| `PERMISSION_DENIED` | Utilizador OAuth não tem acesso à conta de destino |
Níveis de Acesso à API
O acesso à API Google Ads é escalonado, não binário:
| Nível | Provisionamento | Leitura (Produção) | Escrita (Produção) | Escrita (Contas de Teste) | Limite Diário de Operações |
|---|---|---|---|---|---|
| **Test Access** | Imediato do API Center da MCC | ✅ | ❌ | ✅ | 15.000 |
| **Basic Access** | Candidatura + Revisão de compliance | ✅ | ✅ | ✅ | 15.000 |
| **Standard Access** | Limiar de investimento + revisão | ✅ | ✅ | ✅ | Ilimitado |
O Test Access é suficiente para desenvolvimento: autenticação, consultas GAQL, testes validate_only e leitura/escrita completas em contas de teste do Google Ads. A passagem para Basic Access é condicionada por revisão de compliance, não por investimento.
O Standard Access remove o limite diário de operações. Requer um histórico de investimento gerido através do token ou uma candidatura separada.
Contas de Teste
Contas de teste são contas Google Ads gratuitas, sem faturação. Aceitam qualquer mutação. Para criar uma: na MCC, vá a Contas → + → Criar nova conta → Conta de teste. Vincule-a à MCC e utilize-a como customer_id de destino durante o desenvolvimento com Test Access.
`validate_only` Entre Níveis de Acesso
validate_only=True funciona em todos os níveis de acesso, mas o Test Access continua a rejeitar mutações validate_only em contas de produção com RESOURCE_NOT_FOUND. Isto não é uma limitação do validate_only — é o mesmo controlo de acesso aplicado a todas as operações de escrita, independentemente da flag validate_only.
Acompanhamento de Conversões: Leitura da API vs. Estado Funcional
A interface do Google Ads pode mostrar "Configurar acompanhamento de conversões" como recomendação mesmo quando existe uma ação de conversão ativa. A API pode confirmar a existência de uma ação de conversão (tipo WEBPAGE, categoria PURCHASE, estado ENABLED, include_in_conversions_metric: true), mas isto apenas prova que o objeto da ação existe. Não prova que o website emite o evento AW-.../label correspondente, que a Google tag está carregada ou que um sinal de conversão foi recebido.
Ao auditar o acompanhamento de conversões, separe a verificação da API (metadados da ação de conversão) da verificação no navegador (separador de rede, carregamento de tags, disparo de eventos). A API é a fonte de verdade para a configuração; o navegador é a fonte de verdade para a execução.
O padrão de atribuição de receita no backend aplica-se aqui: se os eventos de conversão do lado do cliente não forem fiáveis, planeie o upload de conversões via servidor ou offline através do ConversionUploadService da Google Ads API como alternativa.
Versionamento da API
A biblioteca Python google-ads é distribuída com várias versões da API. Em junho de 2026, estão disponíveis as versões v19 a v22. Cada versão adiciona, descontinua ou remove campos GAQL e métodos de serviço.
Regras para seleção de versão:
load_from_storage(version='v22'). A padrão pode estar desatualizada.search_stream está disponível a partir da v6+ e é o caminho de leitura recomendado.use_proto_plus: true) é obrigatório para v12+.mutate_campaigns(request=req)) substituiu o estilo de argumentos nomeados na v17+.Segurança Local
Todos os ficheiros de credenciais devem permanecer fora do controlo de versões:
~/.hermes/
google-ads.yaml # 0600 — dev token, cliente OAuth, refresh token
google_ads_token.json # 0600 — tokens OAuth, scope adwords
google_client_secret.json # 0600 — OAuth client ID + secret
Para o repositório de trabalho, adicione um .gitignore:
google-ads.yaml
*_token*.json
*_secret*.json
Um ficheiro de modelo com valores placeholder pode ser submetido para documentar a estrutura esperada:
# google-ads.yaml.template — submeter este, preencher localmente
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
Campos Obrigatórios para Operações Comuns
| Operação | Campos Obrigatórios |
|---|---|
| Criar Campanha (Search) | `name`, `status`, `advertising_channel_type`, `campaign_budget`, `manual_cpc` ou estratégia de lance |
| Criar Grupo de Anúncios (Search) | `name`, `status`, `type_`, `cpc_bid_micros`, `campaign` |
| Criar Palavra-chave | `ad_group`, `status`, `keyword.text`, `keyword.match_type` |
| Atualizar Estado da Campanha | `resource_name`, `status`, `update_mask` (paths: `['status']`) |
| Atualizar Orçamento | `resource_name`, `amount_micros`, `update_mask` (paths: `['amount_micros']`) |
A falta de um campo obrigatório devolve REQUIRED com o nome do campo. A ausência de update_mask numa operação de atualização é silenciosamente ignorada — o campo não é atualizado e nenhum erro é devolvido.
Script de Teste Completo
Um único script que lê todos os tipos de entidade e executa testes de escrita validate_only:
# google_ads_api_test.py — ler todas as entidades + testes de escrita 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')
# Ler todas as entidades
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")
# Testes de escrita
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.')
Versão executável completa no skill de operações da Google Ads API no repositório de operações publicitárias.