Google Ads API: Configuración, Autenticación, Operaciones de Lectura y Escritura
Una guía técnica para configurar el acceso programático a Google Ads — desde la arquitectura hasta la primera llamada a la API. Cubre la configuración del MCC, OAuth 2.0, niveles del Developer Token, la librería Python google-ads, consultas GAQL de lectura y el patrón de escritura validate_only.
Arquitectura
Se requieren tres entidades separadas antes de que una sola llamada a la API funcione:
| Entidad | Propósito | Ejemplo |
|---|---|---|
| **Cuenta de Gestor (MCC)** | Emite el Developer Token | `login_customer_id` |
| **Proyecto de Google Cloud** | Cliente OAuth 2.0 para autenticación | Client ID + Secret |
| **Cuenta de Google Ads** | Destino de todas las operaciones de la API | `customer_id` |
No son la misma cuenta. Un MCC puede poseer un Developer Token sin ser propietario de la cuenta destino — solo necesita estar vinculado como gestor. El proyecto de Google Cloud puede estar bajo una identidad de Google diferente a la del MCC. La cuenta destino es la cuenta de anunciante donde residen las campañas.
Esta separación es importante porque API Center (donde se aprovisionan los Developer Tokens) solo está disponible dentro de Cuentas de Gestor. Una cuenta de Google Ads independiente no puede acceder a API Center, incluso si tiene usuarios administrativos.
La cabecera de la llamada a la API ensambla estas tres identidades más un token Bearer de OAuth:
developer-token: <from MCC API Center>
login-customer-id: <MCC numeric ID, no dashes>
customer-id: <target account numeric ID>
Authorization: Bearer <OAuth 2.0 access token>
Requisitos Previos
- Una Cuenta de Gestor de Google Ads con configuración completada
- La cuenta de anuncios destino vinculada a la Cuenta de Gestor y el vínculo aceptado
- Un proyecto de Google Cloud con la API de Google Ads habilitada
- Python 3.9+ con la librería
google-ads(pip install google-ads) - Un navegador para el flujo único de consentimiento OAuth
Paso 1: Cuenta de Gestor y Developer Token
API Center se encuentra en Herramientas y Configuración → Configuración → API Center dentro de una Cuenta de Gestor. Si API Center no aparece, la cuenta o bien no es una Cuenta de Gestor o su configuración está incompleta.
Desde API Center, solicita un Developer Token. Establece el tipo de cuenta como Anunciante (no Agencia ni Terceros) cuando el token es para tus propias cuentas. El token se emite inmediatamente con nivel de Acceso de Prueba.
El Acceso de Prueba permite:
El error para escrituras contra cuentas de producción con Acceso de Prueba es RESOURCE_NOT_FOUND — no PERMISSION_DENIED. Esto es confuso porque el recurso sí existe y es legible. El error significa que la API se niega a enrutar la mutación bajo el nivel de acceso actual.
Solicitud de Acceso Básico
Para escribir contra cuentas de producción, solicita Acceso Básico desde la misma página de API Center. El formulario de solicitud contiene 12 preguntas. Para uso interno en tus propias cuentas, las respuestas relevantes son:
| Pregunta | Respuesta |
|---|---|
| Q4: Relación con un Representante de Google | No |
| Q6: Modelo de Negocio | Describe tu caso de uso (e-commerce, generación de leads, reporting interno) |
| Q8: Quién tendrá acceso | Usuarios internos — solo empleados |
| Q9: Herramienta desarrollada por terceros | No (scripts/agentes propios) |
| Q10: API de Seguimiento de Conversiones y Remarketing | No (a menos que realmente la uses) |
| Q11: Tipos de Campaña | Search, Performance Max, Shopping (según corresponda) |
| Q12: Capacidades | Creación de Campañas, Gestión de Campañas, Reporting |
Se requiere un documento de diseño (PDF) para Q7. Debe describir la arquitectura de la API, el flujo de autenticación, la estrategia de rate-limiting, el manejo de errores y el caso de uso exclusivamente interno. Mantenlo factual — el equipo de cumplimiento revisa la consistencia con el caso de uso declarado.
Tiempo de procesamiento a junio de 2026: aproximadamente 3 días hábiles, con una nota sobre un volumen de solicitudes superior al habitual.
Paso 2: OAuth 2.0
Configuración del Proyecto en Google Cloud
En la Consola de Google Cloud, dentro del proyecto correspondiente:
- Habilita la API de Google Ads (APIs y Servicios → Biblioteca)
- Configura la pantalla de consentimiento OAuth:
https://www.googleapis.com/auth/adwordsEl tipo Desktop app usa http://localhost como URI de redirección. Esto es correcto — el flujo OAuth abre un navegador, Google redirige a localhost con el código de autorización, y un servidor HTTP local lo captura.
Generación del Refresh Token
Un script Python que ejecuta un servidor HTTP local en 127.0.0.1:0 (puerto aleatorio), imprime la URL de autorización, espera una solicitud, extrae el parámetro code y lo intercambia 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())
Dos parámetros son importantes:
access_type='offline' — devuelve un refresh token, no solo un access tokenprompt='consent' — fuerza una nueva pantalla de consentimiento incluso si el usuario autorizó previamente; necesario para obtener un nuevo refresh token si los scopes cambiaronEl refresh token es permanente a menos que se revoque, el usuario cambie su contraseña o el token no se use durante 6 meses.
Problema de Scope con Clientes Existentes
Si el cliente OAuth fue autorizado previamente para otros scopes (Gmail, Drive, Calendar), Google devuelve la unión de los scopes antiguos y nuevos. La librería google-auth-oauthlib rechaza esta discrepancia de scopes por defecto. Establece la variable de entorno antes de cargar el flujo:
import os
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
Alternativamente, omite include_granted_scopes=true de la URL de autorización, lo que le indica a Google que solicite solo el scope explícitamente pasado.
Paso 3: `google-ads.yaml`
La librería lee la configuración de un archivo YAML:
developer_token: <value>
client_id: <value>
client_secret: <value>
refresh_token: <value>
login_customer_id: 1911599764
use_proto_plus: true
Reglas de seguridad:
0600 (solo lectura/escritura del propietario)login_customer_id es el ID numérico del MCC sin guiones. Le indica a la API qué Developer Token de cuenta de gestor usar. El customer_id destino se pasa en el momento de la llamada, no en la configuración.
use_proto_plus: true habilita la interfaz protobuf-plus, que es necesaria para las versiones actuales de la API.
Paso 4: Inicialización del 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')
El parámetro version selecciona explícitamente la versión de la API. La librería google-ads incluye múltiples versiones instaladas en paralelo (v19 a v22 a junio de 2026). Especifica siempre la versión — la predeterminada puede estar desactualizada o no soportar campos más recientes.
Verificar conectividad:
cs = client.get_service('CustomerService')
customers = cs.list_accessible_customers()
ids = [r.split('/')[-1] for r in customers.resource_names]
# Example output: ['1305475941', '3977581086', '1911599764']
Paso 5: Operaciones de Lectura (GAQL)
Google Ads usa GAQL (Google Ads Query Language), una sintaxis similar a SQL. Todas las operaciones de lectura pasan por GoogleAdsService.search_stream(), que evita la paginación y es preferible a search() para la mayoría de las consultas.
Campañas con Métricas de Rendimiento
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
Presupuestos de Campaña
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 Anuncios
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
Palabras Clave (por Impresiones)
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
Anuncios con 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
Acciones de Conversión
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
Patrón de Ejecución en 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
Paso 6: Operaciones de Escritura con `validate_only`
La API v22 requiere objetos de solicitud, no argumentos keyword. Cada servicio de mutación (CampaignService, AdGroupService, AdGroupCriterionService, CampaignBudgetService) sigue el mismo patrón:
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(), not .add()
client.get_service('CampaignService').mutate_campaigns(request=req)
Diferencias clave respecto a versiones anteriores de la API:
validate_only es un campo en el objeto de solicitud, no un argumento keyword del método mutatereq.operations.append(op), no req.operations.add(op)'customers/1305475941/campaigns/22479990461'), no valores de retorno de métodos auxiliaresPuerta de Mutación en Tres Fases
Por seguridad en producción, las mutaciones pasan por tres puertas:
Puerta 1 — validate_only=True: La API valida la estructura de la solicitud, los campos obligatorios y las referencias a recursos. No se crea ningún objeto. Ejecuta esto primero para cada mutación.
Puerta 2 — Creación en Pausa: Establece validate_only=False y crea el objeto con status=PAUSED. Verifica el resultado con una consulta de lectura antes de continuar.
Puerta 3 — Activación en Vivo: Una llamada API separada que actualiza solo el campo status a ENABLED. Esto debe requerir aprobación explícita, no integrarse en la creación.
# Gate 3: enable a previously created paused campaign
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)
Actualización de Presupuesto
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 per day
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['amount_micros']))
req.operations.append(op)
client.get_service('CampaignBudgetService').mutate_campaign_budgets(request=req)
Actualización de Puja de Grupo de Anuncios
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)
Creación de Palabra Clave
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)
Manejo de Errores
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 error comunes y su significado:
| Código de Error | Causa |
|---|---|
| `RESOURCE_NOT_FOUND` | Token de Acceso de Prueba contra una cuenta de producción (esperado) o recurso genuinamente inexistente |
| `REQUIRED` | Falta un campo obligatorio (ej. estrategia de puja en la creación de campaña) |
| `UNRECOGNIZED_FIELD` | Campo de una versión de API diferente o nombre de campo incorrecto |
| `INVALID_ARGUMENT` | El valor del campo no supera la validación (ej. presupuesto negativo) |
| `PERMISSION_DENIED` | El usuario OAuth no tiene acceso a la cuenta destino |
Niveles de Acceso a la API
El acceso a la API de Google Ads es escalonado, no binario:
| Nivel | Aprovisionamiento | Lectura (Producción) | Escritura (Producción) | Escritura (Cuentas de Prueba) | Límite Diario de Operaciones |
|---|---|---|---|---|---|
| **Acceso de Prueba** | Inmediato desde API Center del MCC | ✅ | ❌ | ✅ | 15.000 |
| **Acceso Básico** | Solicitud + revisión de cumplimiento | ✅ | ✅ | ✅ | 15.000 |
| **Acceso Estándar** | Umbral de gasto + revisión | ✅ | ✅ | ✅ | Ilimitado |
El Acceso de Prueba es suficiente para desarrollo: autenticación, consultas GAQL, pruebas validate_only y lectura/escritura completa contra cuentas de prueba de Google Ads. El salto al Acceso Básico depende de la revisión de cumplimiento, no del gasto.
El Acceso Estándar elimina el límite diario de operaciones. Requiere o bien un historial de gasto gestionado a través del token o una solicitud por separado.
Cuentas de Prueba
Las cuentas de prueba son cuentas gratuitas de Google Ads sin facturación. Aceptan cualquier mutación. Para crear una: en el MCC, ve a Cuentas → + → Crear nueva cuenta → Cuenta de prueba. Vincúlala al MCC y úsala como customer_id destino durante el desarrollo con Acceso de Prueba.
`validate_only` en los Distintos Niveles de Acceso
validate_only=True funciona en todos los niveles de acceso, pero el Acceso de Prueba sigue rechazando mutaciones validate_only contra cuentas de producción con RESOURCE_NOT_FOUND. Esto no es una limitación de validate_only — es el mismo control de acceso aplicado a todas las operaciones de escritura independientemente del flag validate_only.
Seguimiento de Conversiones: Lectura de API vs. Estado Funcional
La interfaz de Google Ads puede mostrar "Configurar seguimiento de conversiones" como recomendación incluso cuando existe una acción de conversión habilitada. La API puede confirmar la existencia de una acción de conversión (tipo WEBPAGE, categoría PURCHASE, estado ENABLED, include_in_conversions_metric: true), pero esto solo demuestra que el objeto de acción existe. No demuestra que el sitio web emita el evento AW-.../label correspondiente, que la etiqueta de Google esté cargada o que se haya recibido una señal de conversión.
Al auditar el seguimiento de conversiones, separa la verificación por API (metadatos de la acción de conversión) de la verificación por navegador (pestaña de red, carga de etiquetas, emisión de eventos). La API es la fuente de verdad para la configuración; el navegador es la fuente de verdad para la ejecución.
El patrón para atribución de ingresos en backend aplica aquí: si los eventos de conversión del lado del cliente no son fiables, planifica la subida de conversiones server-side u offline mediante el ConversionUploadService de la API de Google Ads como alternativa.
Versionado de la API
La librería Python google-ads incluye múltiples versiones de la API. A junio de 2026, v19 a v22 están disponibles. Cada versión añade, deprecia o elimina campos GAQL y métodos de servicio.
Reglas para la selección de versión:
load_from_storage(version='v22'). La predeterminada puede estar obsoleta.search_stream está disponible desde v6+ y es la ruta de lectura recomendada.use_proto_plus: true) es obligatorio para v12+.mutate_campaigns(request=req)) reemplazó el estilo de argumentos keyword en v17+.Seguridad Local
Todos los archivos de credenciales deben permanecer fuera del control de versiones:
~/.hermes/
google-ads.yaml # 0600 — dev token, OAuth client, refresh token
google_ads_token.json # 0600 — OAuth tokens, adwords scope
google_client_secret.json # 0600 — OAuth client ID + secret
Para el repositorio de trabajo, añade un .gitignore:
google-ads.yaml
*_token*.json
*_secret*.json
Un archivo de plantilla con valores placeholder puede incluirse en el repositorio para documentar la estructura esperada:
# google-ads.yaml.template — commit this, fill locally
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 Obligatorios para Operaciones Comunes
| Operación | Campos Obligatorios |
|---|---|
| Crear Campaña (Search) | `name`, `status`, `advertising_channel_type`, `campaign_budget`, `manual_cpc` o estrategia de puja |
| Crear Grupo de Anuncios (Search) | `name`, `status`, `type_`, `cpc_bid_micros`, `campaign` |
| Crear Palabra Clave | `ad_group`, `status`, `keyword.text`, `keyword.match_type` |
| Actualizar Estado de Campaña | `resource_name`, `status`, `update_mask` (paths: `['status']`) |
| Actualizar Presupuesto | `resource_name`, `amount_micros`, `update_mask` (paths: `['amount_micros']`) |
La ausencia de un campo obligatorio devuelve REQUIRED con el nombre del campo. La ausencia de update_mask en una operación de actualización se ignora silenciosamente — el campo no se actualiza y no se devuelve ningún error.
Script de Prueba Completo
Un único script que lee todos los tipos de entidad y ejecuta pruebas de escritura validate_only:
# google_ads_api_test.py — read all entities + validate_only write tests
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')
# Read all entities
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")
# Write tests
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.')
Versión ejecutable completa en el skill de operaciones de Google Ads API en el repositorio de operaciones publicitarias.