SteerAds
Google AdsTutorielOptimisation

Automazione Google Ads API in Python: guida principianti

Primo setup Google Ads API in Python: OAuth2, query GAQL, mutazioni, gestione errori e retry logic. Ecco esattamente gli snippet da incollare, con un repo GitHub pubblico da forkare per iniziare in meno di 30 minuti.

Matt
MattTracking & Data Lead
···11 min di lettura

L'API Google Ads supporta 10.000 operazioni al giorno per account client in Basic Access e un timeout di 1 ora max per query, contro 30 minuti CPU e 50 script max per Google Ads Scripts (documentazione ufficiale quote API). Sugli account osservati nei benchmark Google Ads pubblici, i team data che passano da Scripts all'API in Python recuperano da 5 a 12 ore a settimana sulle pipeline di reporting e audit, e sbloccano casi d'uso impossibili lato Scripts (BigQuery sync, batch mutation 5.000+ entita, integrazione CRM bidirezionale).

Questa guida e un setup passo-passo Python per principianti. Ecco esattamente il comando da incollare, lo snippet OAuth che funziona al primo colpo, la prima query GAQL, e il repo GitHub pubblico da forkare per iniziare. Niente teoria marketing, niente "scoprite le possibilita infinite dell'API" — codice che gira. Prerequisiti: Python 3.9+, un account Google Ads, 30 minuti. Se avete gia dimestichezza con Google Ads Scripts, leggete prima la nostra guida dei 10 script Google Ads pronti da incollare che pone le basi di automazione che l'API estendera. Il nostro calcolatore di spreco budget stima gli EUR bruciati/mese per broad senza negativi o bounce LP eccessivo.

Perche l'API Google Ads quando Scripts esiste gia?

Google Ads Scripts e potente ma limitato: 30 minuti CPU max per run, 50 script attivi max per account, JavaScript ES5 (niente pacchetti npm), nessun accesso a librerie esterne scientifiche (numpy, pandas, scikit-learn). L'API Google Ads e il livello superiore: Python, Java, Go, .NET o Ruby a scelta, integrazione possibile con qualsiasi stack data (BigQuery, Snowflake, Airflow, dbt), batch operation, async, scaling verticale e orizzontale senza limiti lato Google.

Il criterio di passaggio e semplice. Restate su Scripts se: monitorate 1 account o un MCC ristretto, le vostre automazioni rientrano in 30 minuti di runtime, non avete bisogno di librerie Python esterne, il vostro team non vuole mantenere infrastruttura. Passate all'API se: gestite 10+ account da pilotare in parallelo, sincronizzate con un data warehouse (BigQuery, Snowflake), integrate un CRM (HubSpot, Salesforce) in bidirezionale, o esponete le operazioni Google Ads in un prodotto interno (dashboard, automation tool).

Il sweet spot operativo: usate Scripts per il monitoraggio single-account e le automazioni semplici (allarme budget, negativi auto), passate all'API per le pipeline data, il multi-account, l'integrazione CRM. Sugli account osservati nei benchmark Google Ads pubblici, circa il 30-40% delle strutture mature (>500k EUR/anno di spesa) combinano i due: Scripts per il tattico quotidiano, API per lo strategico settimanale e i data sync.

Una domanda torna spesso in formazione: "ho gia 4-5 Scripts che girano, devo migrare tutto?" La risposta pragmatica e no. La migrazione verso l'API e redditizia solo se guadagnate in capacita (multi-account, data warehouse, librerie scientifiche) o se perdete tempo sulle limitazioni Scripts (timeout 30 min, niente pandas). Per un account mid-size con Scripts che girano bene, mantenere il codice in posizione e aggiungere l'API solo sui nuovi casi d'uso e la strategia che minimizza il rischio. Le due pipeline coesistono senza conflitti: Scripts si esegue lato Google, la vostra API gira lato infrastruttura, non si disturbano a vicenda.

L'altro trade-off spesso dimenticato riguarda il costo totale di possesso. Scripts e gratuito lato infra (Google ospita), ma richiede JavaScript in una sandbox limitata — quindi ore di dev per aggirare le limitazioni. L'API richiede Python, un'infrastruttura (Cloud Run, Lambda, EC2, o semplice cron su VPS), monitoraggio, gestione dei secret, e dipendenze da mantenere. Su 12 mesi, un setup API Python per 3-5 script costa tipicamente tra 300 e 1.200 EUR di infra cloud, piu da 20 a 60 ore di dev/manutenzione. Oltre 10 script o 20 account da pilotare, il ROI bascula chiaramente verso l'API.

Setup ambiente Python: OAuth2, credentials, library

Il setup si compone di 6 step: creare un progetto GCP, generare le credenziali OAuth2, recuperare il developer_token Google Ads, installare la library, generare il refresh_token, testare con una query GAQL. Contate 30 minuti per il tutto. Ecco esattamente la procedura.

Step 1 — Progetto GCP e attivazione API

Su console.cloud.google.com, creare un nuovo progetto (nome libero, es: google-ads-api-prod). In APIs and Services > Library, cercare "Google Ads API" e cliccare Enable. L'API e gratuita, ma richiede l'attivazione esplicita per progetto GCP.

Step 2 — Credenziali OAuth2 (Desktop app type)

In APIs and Services > Credentials, cliccare Create Credentials > OAuth client ID. Tipo: Desktop app. Dare un nome esplicito (es: google-ads-api-cli). Scaricare il JSON, conservare client_id e client_secret. Questi due valori alimenteranno il google-ads.yaml.

Step 3 — Developer token lato Google Ads

In Google Ads, Tools and Settings > API Center. Se non ancora fatto, richiedere un developer_token. Il token iniziale e in modalita Test (15.000 ops/giorno, solo account test). Per passare in Basic Access (10.000 ops/giorno, account prod), inviare una richiesta con descrizione del caso d'uso — Google risponde entro 1-5 giorni lavorativi. Per Standard Access (illimitato), contate da 2 a 4 settimane di revisione.

Step 4 — Installare la library e generare il refresh_token

Installazione della library ufficiale:

# Python 3.9+ richiesto
pip install google-ads
# Verificare la versione (24.0.0+ corrisponde all'API v17)
pip show google-ads

Per generare il refresh_token, il modo piu semplice e usare lo script di auth ufficiale fornito da Google:

# Clonare gli esempi ufficiali
git clone https://github.com/googleads/google-ads-python.git
cd google-ads-python/examples/authentication

# Lanciare lo script di generazione
python generate_user_credentials.py \
  --client_id YOUR_CLIENT_ID \
  --client_secret YOUR_CLIENT_SECRET

Lo script apre una pagina OAuth nel vostro browser. Validate l'accesso all'account Google Ads. Lo script stampa un refresh_token nel formato 1//0g...XXXXX. Copiatelo immediatamente, verra mostrato una sola volta.

Step 5 — Configurare il google-ads.yaml

Creare un file google-ads.yaml alla radice del progetto:

# google-ads.yaml — DA AGGIUNGERE AL .gitignore IMMEDIATAMENTE
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 parent senza trattini
use_proto_plus: true

Il login_customer_id e l'ID del vostro MCC parent senza trattini (es: 123-456-7890 diventa 1234567890). E l'account sotto il quale l'API autentica ogni query. Se interrogate un account client di questo MCC, specificherete il customer_id dell'account client nella query stessa.

Sicurezza credenziali critica :

Aggiungete google-ads.yaml al .gitignore PRIMA del primo commit. Un refresh_token esposto su GitHub pubblico viene rilevato dai bot in meno di 30 minuti e puo essere usato per addebitare spesa sul vostro account. In produzione, caricare il YAML da un secret manager (AWS Secrets Manager, GCP Secret Manager, Vault) — mai in plain text sul server.

Step 6 — Testare il setup con una query semplice

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

CUSTOMER_ID = "1112223333"  # ID account client (non il 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()

Lanciare python test_setup.py. Se vedete 5 nomi di campagne visualizzati, il setup e corretto. Se errore INVALID_CUSTOMER_ID, verificate il formato (10 cifre senza trattini). Se errore NOT_ADS_USER, il refresh_token e legato a un account Google che non ha accesso al customer_id specificato.

Prima query GAQL: campaign performance

GAQL (Google Ads Query Language) e il linguaggio di query dell'API Google Ads. Sintassi vicina al SQL ma con specificita: nessun JOIN esplicito (le risorse sono annidate), campi gerarchici (campaign.id, metrics.clicks, segments.date), e un DURING per i date range invece di WHERE date BETWEEN.

Ecco uno script completo che recupera le performance delle campagne ENABLED negli ultimi 30 giorni, con impressioni, clic, costo, conversioni, CTR, CPC, CPA. Il nostro calcolatore CPA in 2 input restituisce il valore + mediana Italia per il vostro verticale.

# 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_eur = row.metrics.cost_micros / 1_000_000  # micros -> EUR
            cpa = cost_eur / 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_eur": round(cost_eur, 2),
                "conversions": row.metrics.conversions,
                "ctr_pct": round(row.metrics.ctr * 100, 2),
                "cpc_eur": round(row.metrics.average_cpc / 1_000_000, 2),
                "cpa_eur": 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_eur']:>7,.2f} EUR | "
              f"{c['conversions']:>5.1f} conv | "
              f"CPA: {c['cpa_eur']}")

Tre punti critici su questa query. Primo: i costi sono in micros (1 EUR = 1.000.000 micros). Dividere sempre per 1_000_000 per avere l'EUR. Secondo: metrics.ctr e un float tra 0 e 1, moltiplicare per 100 per avere una percentuale. Terzo: la clausola DURING LAST_30_DAYS e equivalente a WHERE segments.date BETWEEN '2026-03-28' AND '2026-04-26' ma molto piu leggibile. La lista delle costanti DURING: TODAY, YESTERDAY, LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, THIS_MONTH, LAST_MONTH, ecc. (lista completa).

Tre trappole supplementari spesso incontrate all'inizio della pratica GAQL. *Trappola 1: niente SELECT . L'API impone di dichiarare ogni campo esplicitamente. Elencare 25 campi a mano e verboso ma e intenzionale — Google vuole limitare la banda e forzare l'inserzionista a sapere cosa consuma. Mantenere una costante Python CAMPAIGN_FIELDS = [...] riutilizzabile evita di riscrivere la lista a ogni script. Trappola 2: segments.date introduce sempre row-fanning. Una query senza segments.date aggrega su tutto il periodo del DURING; con segments.date, ottenete una riga per campagna per giorno, quindi 30x piu righe. Scegliere consapevolmente secondo il bisogno (totali periodo vs serie temporale). Trappola 3: ORDER BY e obbligatorio per la paginazione consistente. L'API pagina automaticamente oltre 10.000 righe; senza ORDER BY esplicito, l'ordine delle pagine non e garantito e rischiate di perdere entita durante un'elaborazione batch.

Per testare altre query GAQL in modo interattivo senza Python, Google fornisce un GAQL Query Builder nella documentazione ufficiale — e il modo piu rapido di iterare sulla struttura della query prima di codificarla. Un consiglio pratico: prototipare la query nel query builder, copiarla e incollarla nel vostro script Python, e solo dopo aggiungere il mapping verso le colonne BigQuery o pandas. Evitare di riscrivere la query tre volte durante il debug perche si e dimenticato un campo.

Mutazioni: creare, aggiornare, mettere in pausa una campagna

Le mutazioni sono le operazioni di scrittura dell'API: creare una campagna, modificare un budget, mettere in pausa una keyword, aggiungere un negativo. Passano attraverso i servizi dedicati (CampaignService, CampaignBudgetService, KeywordPlanService, ecc.) e usano il pattern operation > mutation > response.

Ecco uno script che mette in pausa una campagna tramite il suo 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")

    # Costruzione del resource_name (formato obbligatorio)
    resource_name = campaign_service.campaign_path(customer_id, campaign_id)

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

    # Field mask (specificare cosa si aggiorna)
    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"Paused campaign: {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)

Il pattern critico per TUTTE le mutazioni: resource_name + update_mask. Il resource_name identifica l'entita (customers/{customer_id}/campaigns/{campaign_id}), l'update_mask specifica quali campi si modificano (senza, l'API restituisce INVALID_FIELD_MASK). Il protobuf_helpers.field_mask(None, campaign._pb) genera automaticamente la mask dai campi modificati.

Per creare una nuova campagna (Search Standard, budget giornaliero 100 EUR, Manual CPC):

# 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_eur):
    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_eur * 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  # creare in PAUSED, attivare dopo revisione
    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_eur=100)
    print(f"Budget created: {budget_rn}")

    campaign_rn = create_search_campaign(
        client, CUSTOMER_ID, "Test API Campaign", budget_rn
    )
    print(f"Campaign created (PAUSED): {campaign_rn}")

Buone pratiche lato mutazioni:

  • Creare sempre in PAUSED all'inizio, attivare manualmente dopo revisione. Una campagna creata in ENABLED per errore puo spendere il budget in poche ore.
  • Loggare il resource_name restituito dall'API per tracciabilita.
  • Wrappare in try/except GoogleAdsException sistematicamente (vedi sezione retry).
  • Testare su un account test prima di toccare la prod. L'API non propone una modalita dry-run nativa (a differenza di Scripts).

Gestione errori e retry logic in produzione

L'API Google Ads puo restituire 3 categorie di errori: transitori (RESOURCE_EXHAUSTED, DEADLINE_EXCEEDED, UNAVAILABLE) che giustificano un retry con backoff, errori client (INVALID_ARGUMENT, NOT_FOUND, PERMISSION_DENIED) che non avranno mai successo al retry, e rate limit (TOO_MANY_REQUESTS) che richiedono di aspettare la finestra successiva.

Ecco un wrapper di retry esponenziale da incollare in tutti gli script produzione:

# 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 che esegue retry automatico con exponential backoff
    sugli errori transitori dell'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:
                    # Verifica se errore retryable (rate limit in particolare)
                    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:
                        # Errore client non retryable
                        for error in e.failure.errors:
                            print(f"[ERROR] {error.error_code}: {error.message}")
                        raise

            return None

        return wrapper
    return decorator

# Utilizzo
@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))

Il pattern: exponential backoff (il delay raddoppia a ogni retry, con tetto a 60 secondi) + classificazione degli errori (retryable vs non-retryable). Mai fare retry su INVALID_ARGUMENT — e un bug nel vostro codice, non un problema di rete. Mai fare retry indefinitamente — da 3 a 5 retry max, altrimenti mascherate un problema strutturale (token revocato, quota esaurita definitivamente).

Per il logging strutturato, usare logging standard Python con un formato JSON per l'ingestione in uno stack di osservabilita (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)

# Utilizzo
logger.info("Pulled 247 campaigns", extra={"customer_id": CUSTOMER_ID})
Insight chiave :

Sugli account osservati nei benchmark Google Ads pubblici, le pipeline API senza retry logic si bloccano in media da 2 a 4 volte al mese per errori transitori di rete o rate limit, contro da 0 a 1 volta a trimestre con un wrapper retry + backoff. E l'investimento di 30 righe di codice che trasforma uno script da "demo" a "prod-ready".

OAuth refresh flow: quello che si dimentica in produzione

Il refresh_token Google Ads non scade finche viene usato almeno una volta ogni 6 mesi. Tuttavia puo essere revocato in diversi casi da conoscere. Primo caso: l'utente Google che ha generato il token modifica la sua password o la sua MFA — tutti i refresh_token legati a quell'account diventano invalidi. Secondo caso: Google rileva un comportamento sospetto (token usato da 30 IP diversi in 1 ora, per esempio), revoca automaticamente e invia un'email di sicurezza. Terzo caso: superare il limite di 50 refresh_token attivi simultaneamente per OAuth client scatena un FIFO silenzioso che invalida i piu vecchi. E per questa ragione che si raccomanda un account di servizio tecnico dedicato all'API (mai l'account Google personale di un dipendente), con password stabile e MFA hardware key.

Il pattern di retry su errore di auth deve essere diverso dal retry transitorio di rete. Se ricevete UNAUTHENTICATED o PERMISSION_DENIED legato al token, MAI fare retry con backoff — il token non resuscitera. Piuttosto, attivare un allarme (PagerDuty, Slack ops) e lasciare che un umano rigeneri il refresh_token tramite lo script generate_user_credentials.py. Confondere questi due casi puo costare ore di debug e conversioni perse. Per limitare le sorprese, monitorare la salute del token con un cron semplice: una query GAQL minima (SELECT customer.id FROM customer LIMIT 1) ogni 6 ore, allarme se fallimento auth.

Rate limiting: capire le quote in pratica

Le quote API sono strutturate su due livelli: operazioni al giorno (Test 15.000, Basic 10.000 per account client, Standard illimitato ufficiale ma throttle a partire da circa 1 milione/giorno) e query al secondo (limite morbido intorno a 50 RPS sostenuti per OAuth client, sotto il quale l'API risponde senza errore). Un'operazione = una riga mutata, un get, o una pagina di risultati GAQL. Una query GAQL che restituisce 5.000 righe conta per 1 operazione, non 5.000.

La trappola piu frequente in produzione: le batch mutation che superano 5.000 operazioni in una singola chiamata. L'API restituisce allora RESOURCE_EXHAUSTED non a causa della quota giornaliera ma di un limite per-call di 5.000 operazioni. Il pattern corretto e di suddividere le liste: se avete 12.000 keyword negative da aggiungere, fate 3 chiamate da 4.000 keyword piuttosto che una sola da 12.000. Il chunking aggiunge 5 righe di codice e evita il 90% degli errori nelle operazioni bulk.

# Pattern di chunking per batch mutation
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")

Per le pipeline batch notturne, aggiungere un time.sleep(0.5) tra i chunk livella il carico senza degradare il throughput globale. Sugli account osservati nei benchmark Google Ads pubblici, un batch di 50.000 mutazioni suddivise a 4k + sleep 500ms gira in circa 8-10 minuti contro 4-5 minuti in chunk 4k senza sleep — ma con un tasso di fallimento diviso per 5. Il trade-off tempo/affidabilita vale ampiamente lo sleep.

6 script Python pronti da forkare

Per accelerare l'avvio, pubblichiamo un repo GitHub pubblico github.com/steerads/google-ads-python-starter con 6 script Python documentati, pronti da forkare. Ogni script e autonomo, configurato tramite variabili d'ambiente, con retry logic inclusa. Ecco la lista con uno snippet d'esempio per ciascuno.

Script 1 — Pull campaign performance LAST_30_DAYS

Recupera i KPI completi (impressioni, clic, costo, conversioni, ROAS) delle campagne ENABLED, esporta in CSV o invia a BigQuery. Frequenza: giornaliera o oraria. Vedi snippet completo sezione 3.

Script 2 — Update campaign budget in bulk

Modifica i budget giornalieri di N campagne in un'unica batch operation. Utile per i ribilanciamenti budget mensili o gli aggiustamenti stagionali automatici.

# bulk_update_budgets.py
def bulk_update_budgets(client, customer_id, updates):
    """
    updates = [{"budget_id": "111", "new_amount_eur": 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_eur"] * 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 — Add negative keywords from search query report

Recupera il Search Term Performance Report su 30 giorni, identifica le query a 0 conversioni con piu di 15 clic, e le aggiunge come negative a livello campagna. Equivalente dello script 2 della nostra guida Scripts 10 ready-to-copy, ma in API per trattare 100+ campagne in una singola esecuzione.

Script 4 — Pause underperforming keywords (CTR + CPA)

Identifica le keyword con CTR inferiore all'1% E CPA superiore a 2x il CPA target su 30 giorni, le mette in pausa automaticamente con un log prima dell'azione. Criterio multidimensionale impossibile lato Scripts (che obbliga a iterare su AdsApp.keywords()), banale in GAQL.

Script 5 — Export report to BigQuery (data warehousing)

Recupera le metriche aggregate, trasforma con pandas, carica in BigQuery tramite google-cloud-bigquery. E il caso d'uso per il quale l'API supera Scripts: impossibilita di connettere Scripts a BigQuery in modo pulito, mentre lato Python sono 10 righe.

# 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 dati GAQL
    perf = pull_performance(client, customer_id)
    df = pd.DataFrame(perf)
    df["snapshot_date"] = pd.Timestamp.today().normalize()

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

    # 3. Caricamento 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()  # attendere il completamento

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

Script 6 — Daily monitoring with Slack alerts

Combina pull performance + verifica anomalie (spesa, CPA, conversioni) + invio Slack via webhook se anomalia rilevata. Equivalente industrializzato di un cron job di monitoraggio quotidiano.

# daily_monitoring.py
import requests

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

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

def send_slack(alerts):
    if not alerts:
        return
    msg = "Google Ads anomalies detected:\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)

Il repo github.com/steerads/google-ads-python-starter contiene per ogni script: il codice completo documentato, un README con setup passo-passo, un file .env.example per le variabili, e un requirements.txt con versioni fissate per riproducibilita. Per le automazioni cross-platform, consultate anche la nostra guida n8n Google Ads automation flows e la nostra guida MCP Google Ads + Claude Desktop per pilotare Google Ads in modalita conversazionale da Claude.

Quando passare da Scripts all'API

Il passaggio da Scripts all'API non e un upgrade sistematico — e una scelta di trade-off. Ecco i 5 trigger concreti osservati nei benchmark pubblici in audit.

Trigger 1: multi-account (10+ account da pilotare in parallelo). Scripts e mono-account per natura (uno script collegato a un account o un MCC). Oltre 10 account da sincronizzare, l'API Python con un ciclo for sulla lista dei customer_id diventa molto piu semplice da mantenere.

Trigger 2: data warehousing. Se volete inviare le metriche in BigQuery, Snowflake, Redshift, l'API e obbligatoria. Scripts UrlFetchApp puo tecnicamente chiamare un'API REST, ma l'autenticazione GCP, il batching, l'ETL con pandas — richiede Python.

Trigger 3: integrazione CRM bidirezionale. Per inviare conversioni offline da Salesforce/HubSpot, l'API Google Ads supporta OfflineUserDataJobService che fa upload batch sicuro. Consultate la nostra guida conversion tracking Google Ads per il framework funzionale.

Trigger 4: esigenze ML / statistiche avanzate. Rilevamento anomalie statistiche (ARIMA, Prophet), segmentazione di keyword (clustering), forecasting di spesa — tutti questi compiti richiedono numpy/pandas/scikit-learn, impossibili in Scripts.

Trigger 5: prodotto interno. State costruendo una dashboard o un tool interno che visualizza/manipola i dati Google Ads. Per forza API — Scripts non puo esporre una UI o rispondere a una richiesta HTTP.

Per i 5 casi inversi (account unico, monitoraggio semplice, nessun bisogno data, team non-tecnico, infrastruttura vincolata), Scripts resta superiore in costo/manutenzione. La nostra guida Microsoft Ads Scripts copre l'equivalente lato Microsoft.

Per gli account che vogliono industrializzare senza codare il proprio stack, il nostro modulo Auto-ottimizzazione copre l'equivalente dei 6 script qui sopra in modalita managed: pull performance multi-account, anomaly detection, Slack alert, BigQuery sync, senza una riga di Python da mantenere. Consultate anche la nostra checklist di audit Google Ads per la base di audit che deve precedere ogni automazione, e il nostro confronto Zapier vs Make per le automazioni no-code complementari.

Errori comuni da evitare iniziando con l'API

Cinque errori ricorrenti rallentano i principianti Python sull'API Google Ads. Ciascuno costa in media diverse ore di debug evitabili se si conosce la trappola in anticipo. Ecco la lista con diagnosi e correzione diretta.

1. Confondere customer_id e login_customer_id. Diagnosi: lo script restituisce INVALID_CUSTOMER_ID o NOT_ADS_USER anche se tutto sembra ben configurato. Correzione: login_customer_id (nel YAML) = ID del MCC parent sotto il quale l'API si autentica, senza trattini. customer_id (nella query) = ID dell'account client che volete interrogare, senza trattini. Se interrogate direttamente un account che non e sotto un MCC, mettete il suo ID in entrambi i posti. Rimuovere sempre i trattini formato 123-456-7890 -> 1234567890.

2. Dimenticare l'update_mask sulle mutazioni. Diagnosi: la mutazione fallisce con MISSING_REQUIRED_FIELD: update_mask oppure tutti i campi dell'entita vengono sostituiti dai loro default (catastrofe in prod). Correzione: per ogni operazione update, generare la mask tramite protobuf_helpers.field_mask(None, entity._pb) dopo aver impostato i campi. La mask dichiara all'API quali campi state modificando; senza, l'API o rifiuta, o interpreta come "tutti i campi sono al valore di default" e resetta tutto.

3. Ciclare in pull individuale invece di batch. Diagnosi: uno script che deve recuperare 500 campagne impiega 25 minuti invece di 30 secondi. Correzione: MAI fare for campaign_id in ids: ga_service.search(...) con una query per campagna. Invece, una sola query GAQL che filtra WHERE campaign.id IN (...) o recupera tutto e filtra lato Python. L'API non e penalizzata sulla dimensione della query, lo e sul numero di chiamate.

4. Testare direttamente in prod senza account test. Diagnosi: un bug di arrotondamento nel codice di mutazione budget riporta per errore tutti i budget a 1 EUR. La prod brucia. Correzione: creare un account test Google Ads (gratuito, senza carta di credito associata) per tutte le mutazioni in dev. Il developer_token Test copre unicamente gli account test — il che e una protezione involontaria benvenuta. Passare a Basic Access solo dopo aver validato il codice in sandbox su 2-3 casi d'uso reali.

5. Ignorare la paginazione GAQL oltre 10.000 righe. Diagnosi: uno script che recupera le keyword di un account grande si blocca oltre 10k righe o restituisce risultati incompleti senza errore. Correzione: usare ga_service.search_stream(...) che pagina automaticamente senza caricare in memoria l'intero risultato. Per i pull di 50.000+ righe, e obbligatorio — search() carica tutto in memoria e crashera sulle macchine con poca RAM.

Per la documentazione ufficiale, consultate il portale Google Ads API e il repo ufficiale google-ads-python che contiene decine di esempi mantenuti da Google.

Fonti

Fonti ufficiali consultate per questa guida:

FAQ

Serve un developer token per usare l'API Google Ads?

Si, e obbligatorio. Il developer token e legato a un account MCC (manager) e si ottiene tramite Tools and Settings > API Center lato Google Ads. Il token iniziale parte in modalita Test (limitato a 15.000 operazioni/giorno, solo account test), poi Basic Access (10.000 operazioni/giorno, account prod) su richiesta, poi Standard Access (illimitato) dopo revisione di Google. Contate da 1 a 5 giorni lavorativi per Basic, da 2 a 4 settimane per Standard con un dossier che dettaglia il vostro caso d'uso. Per iniziare, il token Test e piu che sufficiente: testare l'OAuth, scrivere le prime query GAQL, debuggare le mutazioni su un account test. Il passaggio Basic arriva solo dopo la validazione del codice in sandbox.

Che differenza c'e tra google-ads-python (ufficiale) e googleads (legacy)?

google-ads-python e la library ufficiale moderna che consuma l'API REST/gRPC v17+ (versioni 2024-2026). googleads era la library legacy che consumava l'API SOAP v201809 (deprecata dal 2022, totalmente disattivata a fine 2023). Se trovate codice googleads o AdWords API nei forum, e obsoleto. Per il 2026, usate UNICAMENTE google-ads-python (pip install google-ads), versione 24.0.0 minima che corrisponde all'API v17. La library espone un client GoogleAdsClient inizializzato da YAML o variabili d'ambiente, con servizi tipizzati (CampaignService, KeywordService, GoogleAdsService per le query GAQL). Documentazione ufficiale su developers.google.com/google-ads/api/docs/client-libs/python.

Quante query GAQL al secondo supporta l'API?

Le quote dipendono dal tier del vostro developer token. Test = 15.000 operazioni/giorno totali, Basic = 10.000 operazioni/giorno per account client, Standard = nessuna quota ufficiale ma throttling lato Google in caso di abuso. Un'operazione = una query GAQL o una mutazione. Per gli account osservati nei benchmark Google Ads pubblici, il pattern dominante e da 200 a 800 query/giorno per uno script di monitoraggio quotidiano su un account mid-size. Il rate limit orario e 10.000 query/ora max per OAuth client. Oltre, l'API restituisce un RESOURCE_EXHAUSTED che va trattato con un exponential backoff (vedi sezione retry logic). Per uno script che deve fare 5.000+ mutazioni, prevedere un batch processing con sleep tra i batch.

L'API puo gestire Smart Bidding e Performance Max?

Si, l'API espone tutte le funzionalita di Google Ads, comprese le piu recenti (Performance Max, Smart Bidding, Demand Gen). Per Performance Max, interagite con CampaignService (campaignType=PERFORMANCE_MAX), AssetGroupService (i gruppi di asset PMax) e ConversionGoalService. Per Smart Bidding, e nei campi bidding_strategy delle campagne (TARGET_CPA, TARGET_ROAS, MAXIMIZE_CONVERSIONS). Limite: la creazione di campagne PMax via API richiede tutti gli asset in parallelo (immagini, headline, descrizioni, sitelink) il che rende il codice piu complesso rispetto alla creazione Search. Per iniziare, meglio creare/modificare campagne Search o Shopping e passare a PMax una volta stabilizzata la pipeline. Consultate la nostra guida Performance Max per la strategia degli asset group.

Come proteggere le credenziali OAuth in produzione?

Tre principi: non committare MAI il google-ads.yaml o i refresh_token su Git (aggiungeteli al .gitignore), usate secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) per lo storage in prod, e fate ruotare i refresh_token periodicamente (90 giorni max consigliato). Per un setup locale di dev, il YAML nella home directory va bene. Per la prod, caricare il YAML dal secret manager all'avvio dell'applicazione. I developer_token, client_id, client_secret possono stare in variabili d'ambiente; il refresh_token deve stare in un secret manager rigido. Se un refresh_token viene esposto (commit GitHub per errore), revocatelo immediatamente tramite Google Cloud Console > APIs and Services > Credentials, rigeneratene uno nuovo, e verificate i log API per rilevare eventuali chiamate malevole.

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