SteerAds
Google AdsTutorielOptimisation

Automación Google Ads API en Python: guía principiantes

Primera configuración Google Ads API en Python: OAuth2, consultas GAQL, mutaciones, gestión de errores y retry logic. He aquí los snippets exactos a pegar, con repo público GitHub para fork y arrancar en menos de 30 minutos.

Matt
MattTracking & Data Lead
···11 min de lectura

La API Google Ads soporta 10.000 operaciones por día por cuenta cliente en Basic Access y un timeout máximo de 1 hora por consulta, frente a 30 minutos de CPU y 50 scripts máximo en el caso de los Google Ads Scripts (documentación oficial de cuotas API). En las cuentas observadas en benchmarks públicos de Google Ads, los equipos de datos que pasan de Scripts a la API en Python recuperan de 5 a 12 horas a la semana en pipelines de reporting y auditoría, y desbloquean casos de uso imposibles del lado Scripts (sync BigQuery, mutaciones batch de 5.000+ entidades, integración bidireccional CRM).

Esta guía es una configuración Python paso a paso para principiantes. He aquí los comandos exactos a pegar, el snippet OAuth que funciona a la primera, la primera consulta GAQL y el repo GitHub público a forkear para arrancar. Sin teoría de marketing, sin "descubra las posibilidades infinitas de la API": solo código que funciona. Requisitos previos: Python 3.9+, una cuenta Google Ads, 30 minutos. Si ya está cómodo con Google Ads Scripts, lea primero nuestra guía de 10 Google Ads Scripts listos para pegar, que sienta las bases de automatización que la API ampliará. Nuestra calculadora de wasted ad spend estima los $ quemados/mes por concordancia amplia sin negativas o por rebote excesivo de la LP.

¿Por qué la API Google Ads cuando ya existen los Scripts?

Google Ads Scripts es potente pero limitado: 30 minutos de CPU como máximo por ejecución, 50 scripts activos como máximo por cuenta, JavaScript ES5 (sin paquetes npm), sin acceso a librerías científicas externas (numpy, pandas, scikit-learn). La API Google Ads es el nivel superior: Python, Java, Go, .NET o Ruby a su elección, integración posible con cualquier stack de datos (BigQuery, Snowflake, Airflow, dbt), batch operations, async, escalado vertical y horizontal sin límite del lado de Google.

El criterio de paso es sencillo. Quédese en Scripts si: monitoriza 1 cuenta o un MCC restringido, sus automatizaciones caben en 30 minutos de runtime, no necesita librerías Python externas, su equipo no quiere mantener infraestructura. Pase a la API si: gestiona 10+ cuentas a pilotar en paralelo, sincroniza con un data warehouse (BigQuery, Snowflake), integra un CRM (HubSpot, Salesforce) de forma bidireccional o expone operaciones Google Ads en un producto interno (dashboard, herramienta de automatización).

El sweet spot operativo: utilice los Scripts para el monitoring monocuenta y las automatizaciones simples (alertas de presupuesto, negativas auto), pase a la API para las pipelines de datos, multicuenta, integración CRM. En las cuentas observadas en benchmarks públicos de Google Ads, en torno al 30 al 40% de las estructuras maduras (más de $500k/año de gasto) combinan ambas: Scripts para la operativa táctica diaria, API para el estratégico semanal y los sync de datos.

Pregunta frecuente en formación: "Ya tengo 4-5 Scripts en producción, ¿tengo que migrarlos todos?". La respuesta pragmática es no. La migración a la API solo es rentable si gana capacidad (multicuenta, data warehouse, librerías científicas) o si pierde tiempo por las limitaciones de Scripts (timeout de 30 min, sin pandas). Para una cuenta mediana cuyos Scripts giran bien, conservar el código en su sitio y añadir la API solo en los nuevos casos de uso es la estrategia que minimiza el riesgo. Las dos pipelines coexisten sin conflicto: Scripts gira del lado de Google, su API gira del lado de su infraestructura, no se pisan.

El otro trade-off a menudo olvidado se refiere al coste total de propiedad. Scripts es gratuito en infraestructura (Google hostea), pero requiere JavaScript en sandbox limitado, así que horas de dev para sortear las limitaciones. La API exige Python, infraestructura (Cloud Run, Lambda, EC2, o un simple cron VPS), monitoring, gestión de secretos y dependencias que mantener. En 12 meses, una configuración API Python para 3-5 scripts cuesta típicamente entre $300 y $1.200 en infraestructura cloud, además de 20 a 60 horas de dev/mantenimiento. Más allá de 10 scripts o 20 cuentas a pilotar, el ROI inclina claramente la balanza hacia la API.

Configuración del entorno Python: OAuth2, credentials, librería

La configuración cabe en 6 pasos: crear un proyecto GCP, generar las credenciales OAuth2, recuperar el developer_token Google Ads, instalar la librería, generar el refresh_token, probar con una consulta GAQL. Cuente con 30 minutos en total. He aquí el procedimiento exacto.

Paso 1: Proyecto GCP y activación de la API

En console.cloud.google.com, cree un nuevo proyecto (cualquier nombre, p. ej. google-ads-api-prod). En APIs and Services > Library, busque "Google Ads API" y haga clic en Enable. La API es gratuita, pero requiere una activación GCP explícita por proyecto.

Paso 2: Credenciales OAuth2 (tipo Desktop app)

En APIs and Services > Credentials, haga clic en Create Credentials > OAuth client ID. Tipo: Desktop app. Asígnele un nombre explícito (p. ej. google-ads-api-cli). Descargue el JSON, conserve client_id y client_secret. Estos dos valores alimentarán google-ads.yaml.

Paso 3: Developer token del lado de Google Ads

En Google Ads, Herramientas y configuración > Centro de API. Si aún no se ha hecho, solicite un developer_token. El token inicial está en modo Test (15.000 ops/día, solo cuentas de prueba). Para pasar a Basic Access (10.000 ops/día, cuentas prod), envíe una solicitud con descripción del caso de uso: Google responde en 1 a 5 días hábiles. Para Standard Access (sin límite), cuente con 2 a 4 semanas de revisión.

Paso 4: Instalación de la librería y generación del refresh_token

Instale la librería oficial:

# Python 3.9+ requerido
pip install google-ads
# Verificar versión (24.0.0+ corresponde a la API v17)
pip show google-ads

Para generar el refresh_token, lo más sencillo es utilizar el script de auth oficial proporcionado por Google:

# Clonar los ejemplos oficiales
git clone https://github.com/googleads/google-ads-python.git
cd google-ads-python/examples/authentication

# Ejecutar el script de generación
python generate_user_credentials.py \
  --client_id YOUR_CLIENT_ID \
  --client_secret YOUR_CLIENT_SECRET

El script abre una página OAuth en su navegador. Valide el acceso a la cuenta Google Ads. El script imprime un refresh_token con el formato 1//0g...XXXXX. Cópielo de inmediato, solo se mostrará una vez.

Paso 5: Configurar google-ads.yaml

Cree un archivo google-ads.yaml en la raíz del proyecto:

# google-ads.yaml: AÑADIR A .gitignore DE INMEDIATO
developer_token: "YOUR_DEVELOPER_TOKEN_22_CHARS"
client_id: "YOUR_CLIENT_ID.apps.googleusercontent.com"
client_secret: "GOCSPX-YOUR_CLIENT_SECRET"
refresh_token: "1//0gYOUR_REFRESH_TOKEN"
login_customer_id: "1234567890"  # MCC padre sin guiones
use_proto_plus: true

El login_customer_id es el ID de su MCC padre sin guiones (p. ej. 123-456-7890 se convierte en 1234567890). Es la cuenta bajo la cual la API se autentica en cada solicitud. Si consulta una cuenta cliente de ese MCC, indicará el customer_id de la cuenta cliente en la propia solicitud.

Seguridad crítica de las credenciales :

Añada google-ads.yaml a .gitignore ANTES del primer commit. Un refresh_token filtrado en GitHub público lo detectan los bots en menos de 30 minutos y puede utilizarse para gastar presupuesto en su cuenta. En producción, cargue el YAML desde un secret manager (AWS Secrets Manager, GCP Secret Manager, Vault); nunca en claro en el servidor.

Paso 6: Probar la configuración con una consulta sencilla

# test_setup.py
from google.ads.googleads.client import GoogleAdsClient

CUSTOMER_ID = "1112223333"  # ID de cuenta cliente (no el MCC)

def test_connection():
    client = GoogleAdsClient.load_from_storage("google-ads.yaml")
    ga_service = client.get_service("GoogleAdsService")

    query = """
        SELECT campaign.id, campaign.name, campaign.status
        FROM campaign
        LIMIT 5
    """

    response = ga_service.search(customer_id=CUSTOMER_ID, query=query)
    for row in response:
        print(f"{row.campaign.id} | {row.campaign.name} | {row.campaign.status.name}")

if __name__ == "__main__":
    test_connection()

Ejecute python test_setup.py. Si ve 5 nombres de campaña mostrados, la configuración es buena. Si error INVALID_CUSTOMER_ID, verifique el formato (10 cifras sin guiones). Si error NOT_ADS_USER, el refresh_token está vinculado a una cuenta Google que no tiene acceso al customer_id especificado.

Primera consulta GAQL: rendimiento de campañas

GAQL (Google Ads Query Language) es el lenguaje de consultas de la API Google Ads. Sintaxis SQL-like pero con especificidades: nada de JOIN explícito (los recursos están anidados), campos jerarquizados (campaign.id, metrics.clicks, segments.date) y un DURING para los rangos de fecha en lugar de WHERE date BETWEEN.

He aquí un script completo que extrae el rendimiento de las campañas ENABLED durante los últimos 30 días, con impresiones, clics, coste, conversiones, CTR, CPC, CPA. Nuestra calculadora de CPA con 2 entradas devuelve el valor + la mediana francesa para su vertical.

# pull_campaign_performance.py
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

CUSTOMER_ID = "1112223333"

def pull_performance(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")

    query = """
        SELECT
            campaign.id,
            campaign.name,
            campaign.status,
            metrics.impressions,
            metrics.clicks,
            metrics.cost_micros,
            metrics.conversions,
            metrics.conversions_value,
            metrics.ctr,
            metrics.average_cpc
        FROM campaign
        WHERE campaign.status = 'ENABLED'
          AND segments.date DURING LAST_30_DAYS
        ORDER BY metrics.cost_micros DESC
        LIMIT 50
    """

    try:
        response = ga_service.search(customer_id=customer_id, query=query)

        results = []
        for row in response:
            cost_usd = row.metrics.cost_micros / 1_000_000  # micros -> USD
            cpa = cost_usd / row.metrics.conversions if row.metrics.conversions > 0 else None

            results.append({
                "id": row.campaign.id,
                "name": row.campaign.name,
                "impressions": row.metrics.impressions,
                "clicks": row.metrics.clicks,
                "cost_usd": round(cost_usd, 2),
                "conversions": row.metrics.conversions,
                "ctr_pct": round(row.metrics.ctr * 100, 2),
                "cpc_usd": round(row.metrics.average_cpc / 1_000_000, 2),
                "cpa_usd": round(cpa, 2) if cpa else None,
            })

        return results

    except GoogleAdsException as ex:
        print(f"Request failed: {ex.error.code().name}")
        for error in ex.failure.errors:
            print(f"  - {error.message}")
        raise

if __name__ == "__main__":
    client = GoogleAdsClient.load_from_storage("google-ads.yaml")
    perf = pull_performance(client, CUSTOMER_ID)

    for c in perf:
        print(f"{c['name'][:40]:40s} | "
              f"{c['impressions']:>8,} impr | "
              f"{c['clicks']:>5,} clicks | "
              f"{c['cost_usd']:>7,.2f} USD | "
              f"{c['conversions']:>5.1f} conv | "
              f"CPA: {c['cpa_usd']}")

Tres puntos críticos sobre esta consulta. Primero: los costes están en micros (1 USD = 1.000.000 micros). Divida siempre por 1_000_000 para obtener USD. Segundo: metrics.ctr es un float entre 0 y 1, multiplique por 100 para obtener un porcentaje. Tercero: la cláusula DURING LAST_30_DAYS equivale a WHERE segments.date BETWEEN '2026-03-28' AND '2026-04-26' pero mucho más legible. La lista de constantes DURING: TODAY, YESTERDAY, LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, THIS_MONTH, LAST_MONTH, etc. (lista completa).

Tres trampas adicionales encontradas a menudo al iniciarse en GAQL. *Trampa 1: nada de SELECT . La API exige declarar explícitamente cada campo. Listar 25 campos a mano es verboso pero es intencionado: Google quiere limitar el ancho de banda y forzar al anunciante a saber lo que consume. Mantener una constante Python CAMPAIGN_FIELDS = [...] reutilizable evita reescribir la lista en cada script. Trampa 2: segments.date siempre introduce row-fanning. Una consulta sin segments.date agrega sobre todo el periodo DURING; con segments.date, obtiene una fila por campaña por día, es decir 30 veces más filas. Elija conscientemente según la necesidad (totales del periodo vs. series temporales). Trampa 3: ORDER BY es obligatorio para una paginación coherente. La API pagina automáticamente más allá de 10.000 filas; sin ORDER BY explícito, el orden de las páginas no está garantizado y se arriesga a saltarse entidades durante un batch processing.

Para probar otras consultas GAQL de forma interactiva sin Python, Google ofrece un GAQL Query Builder en la documentación oficial: es la forma más rápida de iterar sobre la estructura de la consulta antes de programarla. Consejo práctico: prototipe la consulta en el query builder, copie y pegue en su script Python, y solo entonces añada el mapeo a sus columnas BigQuery o pandas. Evite reescribir la consulta tres veces durante el debug porque ha olvidado un campo.

Mutaciones: crear, actualizar, pausar una campaña

Las mutaciones son las operaciones de escritura de la API: crear una campaña, modificar un presupuesto, pausar una palabra clave, añadir una negativa. Pasan por servicios dedicados (CampaignService, CampaignBudgetService, KeywordPlanService, etc.) y utilizan el patrón operation > mutation > response.

He aquí un script que pausa una campaña por su ID:

# pause_campaign.py
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

CUSTOMER_ID = "1112223333"
CAMPAIGN_ID = "111222333"

def pause_campaign(client, customer_id, campaign_id):
    campaign_service = client.get_service("CampaignService")

    # Construir resource_name (formato obligatorio)
    resource_name = campaign_service.campaign_path(customer_id, campaign_id)

    # Operación: update
    campaign_operation = client.get_type("CampaignOperation")
    campaign = campaign_operation.update
    campaign.resource_name = resource_name
    campaign.status = client.enums.CampaignStatusEnum.PAUSED

    # Field mask (especificar lo que actualizamos)
    client.copy_from(
        campaign_operation.update_mask,
        protobuf_helpers.field_mask(None, campaign._pb),
    )

    try:
        response = campaign_service.mutate_campaigns(
            customer_id=customer_id,
            operations=[campaign_operation],
        )
        print(f"Campaña pausada: {response.results[0].resource_name}")

    except GoogleAdsException as ex:
        for error in ex.failure.errors:
            print(f"Error: {error.message}")
            if error.location:
                for field in error.location.field_path_elements:
                    print(f"  Field: {field.field_name}")
        raise

if __name__ == "__main__":
    from google.api_core import protobuf_helpers
    client = GoogleAdsClient.load_from_storage("google-ads.yaml")
    pause_campaign(client, CUSTOMER_ID, CAMPAIGN_ID)

El patrón crítico para TODAS las mutaciones: resource_name + update_mask. El resource_name identifica la entidad (customers/{customer_id}/campaigns/{campaign_id}), el update_mask especifica qué campos se modifican (sin él, la API devuelve INVALID_FIELD_MASK). El protobuf_helpers.field_mask(None, campaign._pb) genera automáticamente la máscara a partir de los campos modificados.

Para crear una nueva campaña (Search estándar, presupuesto diario de $100, CPC manual):

# create_search_campaign.py
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
import uuid

CUSTOMER_ID = "1112223333"

def create_campaign_budget(client, customer_id, daily_budget_usd):
    budget_service = client.get_service("CampaignBudgetService")
    operation = client.get_type("CampaignBudgetOperation")
    budget = operation.create
    budget.name = f"Budget {uuid.uuid4().hex[:8]}"
    budget.amount_micros = int(daily_budget_usd * 1_000_000)
    budget.delivery_method = client.enums.BudgetDeliveryMethodEnum.STANDARD

    response = budget_service.mutate_campaign_budgets(
        customer_id=customer_id, operations=[operation]
    )
    return response.results[0].resource_name

def create_search_campaign(client, customer_id, name, budget_resource):
    campaign_service = client.get_service("CampaignService")
    operation = client.get_type("CampaignOperation")
    campaign = operation.create

    campaign.name = name
    campaign.advertising_channel_type = client.enums.AdvertisingChannelTypeEnum.SEARCH
    campaign.status = client.enums.CampaignStatusEnum.PAUSED  # crear como PAUSED, activar tras revisión
    campaign.manual_cpc.enhanced_cpc_enabled = False
    campaign.campaign_budget = budget_resource

    # Network settings
    campaign.network_settings.target_google_search = True
    campaign.network_settings.target_search_network = True
    campaign.network_settings.target_content_network = False
    campaign.network_settings.target_partner_search_network = False

    response = campaign_service.mutate_campaigns(
        customer_id=customer_id, operations=[operation]
    )
    return response.results[0].resource_name

if __name__ == "__main__":
    client = GoogleAdsClient.load_from_storage("google-ads.yaml")

    budget_rn = create_campaign_budget(client, CUSTOMER_ID, daily_budget_usd=100)
    print(f"Budget creado: {budget_rn}")

    campaign_rn = create_search_campaign(
        client, CUSTOMER_ID, "Test API Campaign", budget_rn
    )
    print(f"Campaña creada (PAUSED): {campaign_rn}")

Buenas prácticas de mutación:

  • Crear siempre como PAUSED inicialmente, activar manualmente tras revisión. Una campaña creada por error como ENABLED puede gastar el presupuesto en unas pocas horas.
  • Loggear el resource_name devuelto por la API para trazabilidad.
  • Wrap en try/except GoogleAdsException sistemáticamente (consulte la sección retry).
  • Probar en una cuenta de test antes de tocar prod. La API no ofrece un modo dry-run nativo (al contrario que los Scripts).

Gestión de errores y retry logic en producción

La API Google Ads puede devolver 3 categorías de errores: transitorios (RESOURCE_EXHAUSTED, DEADLINE_EXCEEDED, UNAVAILABLE) que justifican un retry con backoff, errores cliente (INVALID_ARGUMENT, NOT_FOUND, PERMISSION_DENIED) que no triunfarán nunca con un retry, y rate limit (TOO_MANY_REQUESTS) que exigen esperar a la siguiente ventana.

He aquí un wrapper de retry exponencial a pegar en todos los scripts de producción:

# retry_helpers.py
import time
from functools import wraps
from google.ads.googleads.errors import GoogleAdsException
from google.api_core.exceptions import (
    DeadlineExceeded, ServiceUnavailable, ResourceExhausted
)

RETRYABLE_ERRORS = (
    DeadlineExceeded,
    ServiceUnavailable,
    ResourceExhausted,
)

def with_retry(max_retries=3, base_delay=2.0, max_delay=60.0):
    """
    Decorator que reintenta automáticamente con exponential backoff
    sobre errores transitorios de la API Google Ads.
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)

                except RETRYABLE_ERRORS as e:
                    if attempt == max_retries:
                        print(f"[RETRY] Max retries reached: {e}")
                        raise

                    delay = min(base_delay * (2 ** attempt), max_delay)
                    print(f"[RETRY] Attempt {attempt + 1}/{max_retries} "
                          f"failed: {type(e).__name__}. Retry in {delay}s...")
                    time.sleep(delay)

                except GoogleAdsException as e:
                    # Verificar si el error es retryable (rate limit en particular)
                    is_retryable = any(
                        err.error_code.quota_error or
                        err.error_code.internal_error
                        for err in e.failure.errors
                    )

                    if is_retryable and attempt < max_retries:
                        delay = min(base_delay * (2 ** attempt), max_delay)
                        print(f"[RETRY] GoogleAdsException retryable. "
                              f"Retry in {delay}s...")
                        time.sleep(delay)
                    else:
                        # Error cliente non-retryable
                        for error in e.failure.errors:
                            print(f"[ERROR] {error.error_code}: {error.message}")
                        raise

            return None

        return wrapper
    return decorator

# Uso
@with_retry(max_retries=3, base_delay=2.0)
def pull_performance_safe(client, customer_id):
    ga_service = client.get_service("GoogleAdsService")
    query = "SELECT campaign.id, campaign.name FROM campaign LIMIT 100"
    return list(ga_service.search(customer_id=customer_id, query=query))

El patrón: exponential backoff (el delay se duplica en cada retry, capado a 60 segundos) + clasificación de errores (retryable vs. non-retryable). Nunca reintente sobre INVALID_ARGUMENT: es un bug en su código, no un problema de red. Nunca reintente indefinidamente: 3 a 5 retries como máximo, de lo contrario enmascara un problema estructural (token revocado, cuota agotada de forma permanente).

Para el logging estructurado, utilice el logging estándar de Python con un formato JSON para ingesta en un stack de observabilidad (Datadog, Grafana Loki):

import logging
import json

class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_obj = {
            "ts": self.formatTime(record),
            "level": record.levelname,
            "msg": record.getMessage(),
            "module": record.module,
        }
        if record.exc_info:
            log_obj["exc"] = self.formatException(record.exc_info)
        return json.dumps(log_obj)

logger = logging.getLogger("google_ads_api")
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)

# Uso
logger.info("Pulled 247 campaigns", extra={"customer_id": CUSTOMER_ID})
Idea clave :

En las cuentas observadas en benchmarks públicos de Google Ads, las pipelines API sin retry logic se caen de media 2 a 4 veces al mes en errores transitorios de red o de rate limit, frente a 0 a 1 vez por trimestre con un wrapper retry + backoff. Es una inversión de 30 líneas de código que pasa un script de "demo" a "prod-ready".

Flujo OAuth refresh: lo que se olvida en producción

El refresh_token Google Ads no caduca mientras se utilice al menos una vez cada 6 meses. Sin embargo, puede revocarse en varios casos a conocer. Primer caso: el usuario Google que generó el token cambia su contraseña o MFA: todos los refresh_tokens vinculados a esa cuenta se vuelven inválidos. Segundo caso: Google detecta un comportamiento sospechoso (token utilizado desde 30 IP diferentes en 1 hora, por ejemplo), revoca automáticamente y envía un correo de seguridad. Tercer caso: superar el límite de 50 refresh_tokens activos a la vez por cliente OAuth desencadena un FIFO silencioso que invalida los más antiguos. Por eso recomendamos una service account técnica dedicada para la API (nunca la cuenta Google personal de un empleado), con contraseña estable y MFA de hardware key.

El patrón retry sobre errores de auth debe ser distinto del retry transitorio de red. Si recibe UNAUTHENTICATED o PERMISSION_DENIED vinculados al token, NUNCA reintente con backoff: el token no resucitará. Active en su lugar una alerta (PagerDuty, Slack ops) y deje que un humano regenere el refresh_token vía el script generate_user_credentials.py. Confundir estos dos casos puede costar horas de debug y conversiones perdidas. Para limitar las sorpresas, monitorice la salud del token con un cron sencillo: una consulta GAQL mínima (SELECT customer.id FROM customer LIMIT 1) cada 6 horas, alerta en caso de fallo de auth.

Rate limiting: comprender las cuotas en la práctica

Las cuotas API se estructuran en dos niveles: operaciones por día (Test 15.000, Basic 10.000 por cuenta cliente, Standard oficialmente sin límite pero con throttling en torno a 1 millón/día) y requests por segundo (soft limit en torno a 50 RPS sostenidas por cliente OAuth, por debajo del cual la API responde sin error). Una operación = una fila mutada, un get o una página de resultados GAQL. Una consulta GAQL que devuelve 5.000 filas cuenta como 1 operación, no 5.000.

La trampa más frecuente en producción: las mutaciones batch que superan 5.000 operaciones en una única llamada. La API devuelve entonces RESOURCE_EXHAUSTED no por la cuota diaria sino por un per-call limit de 5.000 operaciones. El patrón correcto consiste en chunkear las listas: si tiene 12.000 negativas que añadir, haga 3 llamadas de 4.000 negativas en lugar de una única de 12.000. El chunking añade 5 líneas de código y evita el 90% de los errores en operaciones bulk.

# Patrón de chunking para mutaciones batch
def chunked(iterable, size=4000):
    for i in range(0, len(iterable), size):
        yield iterable[i:i + size]

for chunk in chunked(all_operations, size=4000):
    response = service.mutate_negatives(
        customer_id=customer_id, operations=chunk
    )
    print(f"Chunk processed: {len(response.results)} mutations")

Para las pipelines batch nocturnas, añadir un time.sleep(0.5) entre chunks suaviza la carga sin degradar el throughput global. En las cuentas observadas en benchmarks públicos de Google Ads, un batch de 50.000 mutaciones chunkeado en 4k + 500ms sleep se ejecuta en aproximadamente 8-10 minutos frente a 4-5 minutos en chunks de 4k sin sleep, pero con una tasa de fallo dividida por 5. El compromiso tiempo/fiabilidad merece el sleep.

6 scripts Python listos para fork

Para acelerar el arranque, publicamos un repo GitHub público github.com/steerads/google-ads-python-starter con 6 scripts Python documentados, listos para fork. Cada script es autónomo, configurado vía env vars, con retry logic incluida. Aquí la lista con un snippet de ejemplo para cada uno.

Script 1: Pull rendimiento de campañas LAST_30_DAYS

Extrae los KPI completos (impresiones, clics, coste, conversiones, ROAS) de las campañas ENABLED, exporta a CSV o hace push a BigQuery. Frecuencia: diaria u horaria. Vea el snippet completo en la sección 3.

Script 2: Bulk update de presupuestos de campañas

Modifica los presupuestos diarios de N campañas en una única operación batch. Útil para el rebalanceo de presupuesto mensual o los ajustes estacionales automáticos.

# bulk_update_budgets.py
def bulk_update_budgets(client, customer_id, updates):
    """
    updates = [{"budget_id": "111", "new_amount_usd": 150}, ...]
    """
    service = client.get_service("CampaignBudgetService")
    operations = []

    for u in updates:
        op = client.get_type("CampaignBudgetOperation")
        budget = op.update
        budget.resource_name = service.campaign_budget_path(
            customer_id, u["budget_id"]
        )
        budget.amount_micros = int(u["new_amount_usd"] * 1_000_000)

        client.copy_from(
            op.update_mask,
            protobuf_helpers.field_mask(None, budget._pb),
        )
        operations.append(op)

    response = service.mutate_campaign_budgets(
        customer_id=customer_id, operations=operations
    )
    return [r.resource_name for r in response.results]

Script 3: Añadir negativas a partir del search query report

Extrae el Search Term Performance Report en 30 días, identifica las consultas con 0 conversión y más de 15 clics, las añade como negativas a nivel de campaña. Equivalente al script 2 de nuestra guía de 10 Scripts listos para copiar, pero en API para gestionar 100+ campañas en una única ejecución.

Script 4: Pausar palabras clave underperforming (CTR + CPA)

Identifica las palabras clave con CTR por debajo del 1% Y CPA por encima de 2 veces el target CPA en 30 días, las pausa automáticamente con un log pre-acción. Criterio multidimensional imposible del lado Scripts (que obliga a iterar sobre AdsApp.keywords()), trivial en GAQL.

Script 5: Export de reporting a BigQuery (data warehousing)

Extrae las métricas agregadas, transforma con pandas, carga en BigQuery vía google-cloud-bigquery. Es el caso de uso en el que la API supera a los Scripts: imposibilidad de conectar Scripts a BigQuery limpiamente, mientras que del lado Python son 10 líneas.

# bigquery_export.py
from google.cloud import bigquery
import pandas as pd

def export_to_bigquery(client, customer_id, dataset_id, table_id):
    # 1. Pull GAQL data
    perf = pull_performance(client, customer_id)
    df = pd.DataFrame(perf)
    df["snapshot_date"] = pd.Timestamp.today().normalize()

    # 2. BigQuery client
    bq = bigquery.Client()
    table_ref = f"{bq.project}.{dataset_id}.{table_id}"

    # 3. Carga con append
    job_config = bigquery.LoadJobConfig(
        write_disposition=bigquery.WriteDisposition.WRITE_APPEND,
        autodetect=True,
    )
    job = bq.load_table_from_dataframe(df, table_ref, job_config=job_config)
    job.result()  # esperar finalización

    print(f"Loaded {len(df)} rows to {table_ref}")

Script 6: Monitoring diario con alertas Slack

Combina pull performance + check anomalies (gasto, CPA, conversiones) + push Slack vía webhook si se detecta una anomalía. Equivalente industrializado de un cron de monitoring diario.

# daily_monitoring.py
import requests

SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

def detect_anomalies(perf):
    alerts = []
    for c in perf:
        # Gasto > 2x media 7d
        if c.get("cost_usd", 0) > c.get("avg_cost_7d", 0) * 2:
            alerts.append(f"Gasto +100%: {c['name']}")
        # CPA > 3x target
        if c.get("cpa_usd") and c.get("target_cpa") and \
           c["cpa_usd"] > c["target_cpa"] * 3:
            alerts.append(f"CPA x3 target: {c['name']}")
    return alerts

def send_slack(alerts):
    if not alerts:
        return
    msg = "Anomalías Google Ads detectadas:\n" + "\n".join(f"- {a}" for a in alerts)
    requests.post(SLACK_WEBHOOK, json={"text": msg})

if __name__ == "__main__":
    client = GoogleAdsClient.load_from_storage("google-ads.yaml")
    perf = pull_performance(client, CUSTOMER_ID)
    alerts = detect_anomalies(perf)
    send_slack(alerts)

El repo github.com/steerads/google-ads-python-starter contiene para cada script: código completo documentado, un README con setup paso a paso, un archivo .env.example para las variables y un requirements.txt pinned para reproducibilidad. Para automatización cross-platform, consulte también nuestra guía de n8n Google Ads automation flows y nuestra guía MCP Google Ads + Claude Desktop para pilotar Google Ads de forma conversacional desde Claude.

Cuándo pasar de Scripts a la API

Pasar de Scripts a la API no es un upgrade sistemático: es una elección de trade-off. He aquí los 5 disparadores concretos observados en benchmarks públicos durante las auditorías.

Disparador 1: multicuenta (10+ cuentas a pilotar en paralelo). Scripts es monocuenta por esencia (un script vinculado a una cuenta o a un MCC). Más allá de 10 cuentas a sincronizar, la API Python con un for loop sobre la lista de customer_ids se vuelve mucho más simple de mantener.

Disparador 2: data warehousing. Si quiere hacer push de las métricas a BigQuery, Snowflake, Redshift, la API es obligatoria. UrlFetchApp de Scripts puede técnicamente llamar a una API REST, pero la autenticación GCP, el batching, el ETL con pandas: eso requiere Python.

Disparador 3: integración CRM bidireccional. Para empujar las conversiones offline de Salesforce/HubSpot, la API Google Ads soporta OfflineUserDataJobService que hace un upload batch seguro. Consulte nuestra guía de seguimiento de conversiones de Google Ads para el marco funcional.

Disparador 4: necesidades de ML / estadísticas avanzadas. Detección de anomalías estadísticas (ARIMA, Prophet), segmentación de palabras clave (clustering), forecasting de gasto: todas estas tareas exigen numpy/pandas/scikit-learn, imposibles en Scripts.

Disparador 5: producto interno. Construye un dashboard o una herramienta interna que muestra/manipula datos Google Ads. Necesariamente API: Scripts no puede exponer una UI ni responder a una solicitud HTTP.

Para los 5 casos inversos (cuenta única, monitoring sencillo, sin necesidades de datos, equipo no técnico, infraestructura limitada), Scripts sigue siendo superior en coste/mantenimiento. Nuestra guía Microsoft Ads Scripts cubre el equivalente del lado Microsoft.

Para las cuentas que quieran industrializar sin programar su propio stack, nuestro módulo Auto-optimization cubre el equivalente de los 6 scripts anteriores en modo gestionado: pull performance multicuenta, detección de anomalías, alertas Slack, sync BigQuery, sin una línea de Python que mantener. Consulte también nuestra checklist de auditoría de Google Ads para la base de auditoría que debe preceder a cualquier automatización, y nuestra comparativa Zapier vs. Make para automatizaciones complementarias en no-code.

Errores frecuentes a evitar al iniciarse en la API

Cinco errores recurrentes ralentizan a los principiantes Python en la API Google Ads. Cada uno cuesta de media varias horas de debug evitables si se conoce la trampa por adelantado. He aquí la lista con diagnóstico y corrección directa.

1. Confundir customer_id y login_customer_id. Diagnóstico: el script devuelve INVALID_CUSTOMER_ID o NOT_ADS_USER cuando todo parece estar bien configurado. Corrección: login_customer_id (en el YAML) = ID del MCC padre bajo el cual la API se autentica, sin guiones. customer_id (en la solicitud) = ID de la cuenta cliente que quiere consultar, sin guiones. Si consulta directamente una cuenta que no está bajo un MCC, ponga su ID en ambos sitios. Quite siempre los guiones del formato 123-456-7890 -> 1234567890.

2. Olvidar el update_mask en las mutaciones. Diagnóstico: la mutación falla con MISSING_REQUIRED_FIELD: update_mask, o todos los campos de la entidad se sustituyen por sus valores por defecto (catástrofe en prod). Corrección: para cualquier operación update, genere la máscara vía protobuf_helpers.field_mask(None, entity._pb) después de haber definido los campos. La máscara declara a la API qué campos modifica; sin ella, la API o se niega o interpreta como "todos los campos están en su valor por defecto" y resetea todo.

3. Looping en pulls individuales en lugar de batch. Diagnóstico: un script que debe extraer 500 campañas tarda 25 minutos en lugar de 30 segundos. Corrección: NUNCA haga for campaign_id in ids: ga_service.search(...) con una consulta por campaña. Prefiera una única consulta GAQL que filtre WHERE campaign.id IN (...) o que extraiga todo y filtre del lado Python. La API no se penaliza por el tamaño de la consulta, se penaliza por el número de llamadas.

4. Probar directamente en producción sin cuenta de test. Diagnóstico: un bug de redondeo en el código de mutación de presupuesto baja accidentalmente todos los presupuestos a $1. Producción quemada. Corrección: cree una cuenta Google Ads de test (gratuita, sin CB asociada) para todas las mutaciones de dev. El developer_token Test solo cubre las cuentas de test, lo que constituye una protección involuntaria bienvenida. Pase a Basic Access solo tras validar el código en sandbox sobre 2-3 casos de uso reales.

5. Ignorar la paginación GAQL más allá de 10.000 filas. Diagnóstico: un script que extrae las palabras clave de una cuenta grande se cae más allá de 10k filas o devuelve incompleto sin error. Corrección: utilice ga_service.search_stream(...) que pagina automáticamente sin cargar todo el resultado en memoria. Para extracciones de 50.000+ filas, es obligatorio: search() carga todo en memoria y se caerá en máquinas con poca RAM.

Para la documentación oficial, consulte el portal API Google Ads y el repo oficial google-ads-python que contiene decenas de ejemplos mantenidos por Google.

Fuentes

Fuentes oficiales consultadas para esta guía:

FAQ

¿Hay que tener un developer token para utilizar la API Google Ads?

Sí, es obligatorio. El developer token está vinculado a una cuenta MCC (manager) y se obtiene vía Herramientas y configuración > Centro de API del lado de Google Ads. El token inicial está en modo Test (limitado a 15.000 operaciones/día, solo cuentas de prueba), después Basic Access (10.000 operaciones/día, cuentas prod) bajo solicitud, después Standard Access (sin límite) tras revisión por Google. Cuente con 1 a 5 días hábiles para Basic, 2 a 4 semanas para Standard con un dossier que detalle su caso de uso. Para empezar, el token Test es más que suficiente: probar OAuth, escribir sus primeras consultas GAQL, depurar las mutaciones en una cuenta de prueba. El paso a Basic solo se hace tras validar el código en sandbox.

¿Cuál es la diferencia entre google-ads-python (oficial) y googleads (legacy)?

google-ads-python es la librería oficial moderna que consume la API REST/gRPC v17+ (versiones 2024-2026). googleads era la librería legacy que consumía la API SOAP v201809 (deprecada desde 2022, apagada por completo a finales de 2023). Si encuentra código googleads o AdWords API en foros, es obsoleto. Para 2026, utilice ÚNICAMENTE google-ads-python (pip install google-ads), versión 24.0.0 mínimo correspondiente a la API v17. La librería expone un GoogleAdsClient inicializado a partir de YAML o env vars, con servicios tipados (CampaignService, KeywordService, GoogleAdsService para las consultas GAQL). Documentación oficial en developers.google.com/google-ads/api/docs/client-libs/python.

¿Cuántas consultas GAQL por segundo soporta la API?

Las cuotas dependen del tier de su developer token. Test = 15.000 operaciones/día en total, Basic = 10.000 operaciones/día por cuenta cliente, Standard = sin cuota oficial pero throttling del lado de Google en caso de abuso. Una operación = una consulta GAQL o una mutación. Para las cuentas observadas en benchmarks públicos de Google Ads, el patrón dominante es de 200 a 800 consultas/día para un script de monitoring diario en una cuenta mediana. Rate limit por hora de 10.000 consultas/hora máximo por cliente OAuth. Más allá, la API devuelve RESOURCE_EXHAUSTED, que hay que gestionar con un exponential backoff (consulte la sección retry logic). Para un script que deba ejecutar 5.000+ mutaciones, prevea un batch processing con sleep entre lotes.

¿La API gestiona Smart Bidding y Performance Max?

Sí, la API expone todas las funciones Google Ads, incluidas las más recientes (Performance Max, Smart Bidding, Demand Gen). Para Performance Max, interactúe con CampaignService (campaignType=PERFORMANCE_MAX), AssetGroupService (asset groups PMax) y ConversionGoalService. Para Smart Bidding, está en los campos bidding_strategy de las campañas (TARGET_CPA, TARGET_ROAS, MAXIMIZE_CONVERSIONS). Limitación: crear una campaña PMax vía API exige todos los assets en paralelo (imágenes, títulos, descripciones, sitelinks), lo que hace el código más complejo que la creación de Search. Para empezar, prefiera la creación/modificación de campañas Search o Shopping y pase a PMax una vez estabilizado el pipeline. Consulte nuestra guía Performance Max para la estrategia de asset group.

¿Cómo asegurar las credenciales OAuth en producción?

Tres principios: NUNCA commitear google-ads.yaml o los refresh_tokens en Git (añádalos a .gitignore), utilice secret managers (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) para el almacenamiento prod y rote los refresh_tokens periódicamente (90 días máximo recomendado). Para una configuración local de dev, el YAML en home directory está bien. Para prod, cargue el YAML desde el secret manager al iniciar la aplicación. El developer_token, client_id, client_secret pueden estar en env vars; el refresh_token debe estar en secret manager estricto. Si un refresh_token se filtra (commit accidental en GitHub), revóquelo de inmediato vía Google Cloud Console > APIs and Services > Credentials, regenere uno nuevo y audite los logs API para detectar posibles llamadas maliciosas.

Ready to optimize your campaigns?

Start a free audit in 2 minutes and discover the ROI potential of your accounts.

Start my free audit

Free audit — no credit card required

Keep reading