Google Ads API: Configurazione, Autenticazione, Operazioni di Lettura e Scrittura
Una guida tecnica per configurare l'accesso programmatico a Google Ads — dall'architettura alla prima chiamata API. Copre la configurazione MCC, OAuth 2.0, i livelli del Developer Token, la libreria Python google-ads, le query GAQL in lettura e il pattern di scrittura validate_only.
Architettura
Tre entità separate sono necessarie prima che una singola chiamata API funzioni:
| Entità | Scopo | Esempio |
|---|---|---|
| **Account Manager (MCC)** | Rilascia il Developer Token | `login_customer_id` |
| **Google Cloud Project** | Client OAuth 2.0 per l'autenticazione | Client ID + Secret |
| **Account Google Ads** | Target di tutte le operazioni API | `customer_id` |
Non si tratta dello stesso account. Un MCC può possedere un Developer Token senza possedere l'account target — deve solo essere collegato come manager. Il Google Cloud Project può appartenere a un'identità Google diversa dall'MCC. L'account target è l'account inserzionista dove risiedono le campagne.
Questa separazione è importante perché API Center (dove vengono forniti i Developer Token) è disponibile solo all'interno degli Account Manager. Un account Google Ads autonomo non può accedere ad API Center, anche se dispone di utenti amministrativi.
L'header della chiamata API assembla queste tre identità più un token Bearer OAuth:
developer-token: <da API Center dell'MCC>
login-customer-id: <ID numerico MCC, senza trattini>
customer-id: <ID numerico account target>
Authorization: Bearer <token di accesso OAuth 2.0>
Prerequisiti
- Un Account Manager Google Ads con configurazione completata
- L'account ads target collegato all'Account Manager e il collegamento accettato
- Un Google Cloud Project con la Google Ads API abilitata
- Python 3.9+ con la libreria
google-ads(pip install google-ads) - Un browser per il flusso di consenso OAuth una tantum
Passo 1: Account Manager e Developer Token
API Center si trova in Strumenti e impostazioni → Configurazione → API Center all'interno di un Account Manager. Se API Center non è presente, l'account non è un Account Manager oppure la sua configurazione è incompleta.
Da API Center, richiedi un Developer Token. Imposta il tipo di account su Inserzionista (non Agenzia o Terze Parti) quando il token è per i tuoi account. Il token viene rilasciato immediatamente al livello Accesso di Test.
L'Accesso di Test consente:
- Operazioni di lettura su qualsiasi account collegato
- Operazioni di scrittura solo su account di test
- Nessuna operazione di scrittura su account di produzione
L'errore per le scritture su account di produzione con Accesso di Test è RESOURCE_NOT_FOUND — non PERMISSION_DENIED. Questo è fuorviante perché la risorsa esiste ed è leggibile. L'errore significa che l'API rifiuta di instradare la mutation con il livello di accesso corrente.
Richiesta di Accesso Base
Per scrivere su account di produzione, richiedi l'Accesso Base dalla stessa pagina API Center. Il modulo di richiesta contiene 12 domande. Per uso interno sui propri account, le risposte pertinenti sono:
| Domanda | Risposta |
|---|---|
| Q4: Rapporto con un rappresentante Google | No |
| Q6: Modello di business | Descrivi il tuo caso d'uso (e-commerce, lead generation, reportistica interna) |
| Q8: Chi avrà accesso | Utenti interni — solo dipendenti |
| Q9: Strumento sviluppato da terze parti | No (script/agent propri) |
| Q10: API Conversion Tracking e Remarketing | No (a meno che non le utilizzi effettivamente) |
| Q11: Tipi di campagne | Search, Performance Max, Shopping (a seconda dei casi) |
| Q12: Funzionalità | Creazione campagne, Gestione campagne, Reportistica |
Un documento di design (PDF) è richiesto per la Q7. Deve descrivere l'architettura dell'API, il flusso di autenticazione, la strategia di rate-limiting, la gestione degli errori e il caso d'uso esclusivamente interno. Sii fattuale — il team di compliance verifica la coerenza con il caso d'uso dichiarato.
Tempo di elaborazione a giugno 2026: circa 3 giorni lavorativi, con una nota sull'alto volume di richieste superiore al normale.
Passo 2: OAuth 2.0
Configurazione del Google Cloud Project
Nella Google Cloud Console, nel progetto pertinente:
- Abilita la Google Ads API (API e servizi → Libreria)
- Configura la schermata di consenso OAuth:
- Tipo di utente: Interno (solo gli utenti del tuo Google Workspace possono autorizzare — salta la verifica dell'app esterna)
- Nome app, email di supporto, email di contatto sviluppatore
- Aggiungi lo scope in Accesso ai dati:
https://www.googleapis.com/auth/adwords - Crea un ID client OAuth:
- Tipo di applicazione: App desktop
- Scarica il JSON del client secret
Il tipo App desktop utilizza http://localhost come URI di reindirizzamento. È corretto — il flusso OAuth apre un browser, Google reindirizza a localhost con il codice di autorizzazione e un server HTTP locale lo cattura.
Generazione del Refresh Token
Uno script Python che esegue un server HTTP locale su 127.0.0.1:0 (porta casuale), stampa l'URL di autorizzazione, attende una richiesta, estrae il parametro code e lo scambia per i token:
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())
Due parametri sono importanti:
access_type='offline'— restituisce un refresh token, non solo un access tokenprompt='consent'— forza una nuova schermata di consenso anche se l'utente ha già autorizzato in precedenza; necessario per ottenere un nuovo refresh token se gli scope sono cambiati
Il refresh token è permanente a meno che non venga revocato, l'utente cambi la password o il token rimanga inutilizzato per 6 mesi.
Insidia degli Scope con Client Esistenti
Se il client OAuth è stato precedentemente autorizzato per altri scope (Gmail, Drive, Calendar), Google restituisce l'unione degli scope vecchi e nuovi. La libreria google-auth-oauthlib rifiuta questa discrepanza di scope per impostazione predefinita. Imposta la variabile d'ambiente prima di caricare il flusso:
import os
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
In alternativa, ometti include_granted_scopes=true dall'URL di autorizzazione, il che indica a Google di richiedere solo lo scope esplicitamente passato.
Passo 3: `google-ads.yaml`
La libreria legge la configurazione da un file YAML:
developer_token: <valore>
client_id: <valore>
client_secret: <valore>
refresh_token: <valore>
login_customer_id: 1911599764
use_proto_plus: true
Regole di sicurezza:
- Permessi del file:
0600(solo lettura/scrittura proprietario) - Mai committare nel controllo versione
- Conservare al di fuori di qualsiasi repository Git
- Per configurazioni di team, distribuire tramite un secrets manager, non un file condiviso
login_customer_id è l'ID numerico dell'MCC senza trattini. Indica all'API quale Developer Token dell'account manager utilizzare. Il customer_id target viene passato al momento della chiamata, non nella configurazione.
use_proto_plus: true abilita l'interfaccia protobuf-plus, richiesta per le versioni API correnti.
Passo 4: Inizializzazione del Client 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')
Il parametro version seleziona esplicitamente la versione dell'API. La libreria google-ads viene distribuita con più versioni installate in parallelo (da v19 a v22 a giugno 2026). Specifica sempre la versione — il default potrebbe essere in ritardo o non supportare campi più recenti.
Verifica la connettività:
cs = client.get_service('CustomerService')
customers = cs.list_accessible_customers()
ids = [r.split('/')[-1] for r in customers.resource_names]
# Output di esempio: ['1305475941', '3977581086', '1911599764']
Passo 5: Operazioni di Lettura (GAQL)
Google Ads utilizza GAQL (Google Ads Query Language), una sintassi simile a SQL. Tutte le operazioni di lettura passano attraverso GoogleAdsService.search_stream(), che evita la paginazione ed è preferito rispetto a search() per la maggior parte delle query.
Campagne con Metriche di Performance
SELECT
campaign.id,
campaign.name,
campaign.status,
campaign.advertising_channel_type,
campaign.serving_status,
campaign.bidding_strategy_type,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions,
metrics.conversions_value,
metrics.ctr,
metrics.average_cpc
FROM campaign
ORDER BY campaign.id
Budget delle Campagne
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
Gruppi di Annunci
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
Parole Chiave (per Impression)
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
Annunci con Stato delle Policy
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
Azioni di Conversione
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
Pattern di Esecuzione Python
def gaql_query(client, customer_id, query):
gs = client.get_service('GoogleAdsService')
results = []
for batch in gs.search_stream(customer_id=customer_id, query=query):
for row in batch.results:
results.append(row)
return results
Passo 6: Operazioni di Scrittura con `validate_only`
L'API v22 richiede oggetti request, non argomenti keyword. Ogni servizio di mutation (CampaignService, AdGroupService, AdGroupCriterionService, CampaignBudgetService) segue lo stesso pattern:
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 = 'Nome Campagna'
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(), non .add()
client.get_service('CampaignService').mutate_campaigns(request=req)
Differenze chiave rispetto alle versioni precedenti dell'API:
validate_onlyè un campo sull'oggetto request, non un argomento keyword del metodo mutate- Le operazioni vengono aggiunte tramite
req.operations.append(op), nonreq.operations.add(op) - I percorsi delle risorse sono stringhe esplicite (
'customers/1305475941/campaigns/22479990461'), non valori restituiti da metodi helper
Gate di Mutation a Tre Fasi
Per la sicurezza in produzione, le mutation passano attraverso tre gate:
Gate 1 — validate_only=True: L'API convalida la struttura della richiesta, i campi obbligatori e i riferimenti alle risorse. Nessun oggetto viene creato. Esegui questo per primo per ogni mutation.
Gate 2 — Creazione in Pausa: Imposta validate_only=False e crea l'oggetto con status=PAUSED. Verifica il risultato con una query di lettura prima di procedere.
Gate 3 — Attivazione Live: Una chiamata API separata che aggiorna solo il campo status a ENABLED. Questa dovrebbe richiedere un'approvazione esplicita, non essere incorporata nella creazione.
# Gate 3: attivare una campagna precedentemente creata in pausa
req = client.get_type('MutateCampaignsRequest')
req.customer_id = '1305475941'
req.validate_only = False
op = client.get_type('CampaignOperation')
op.update.resource_name = 'customers/1305475941/campaigns/22479990461'
op.update.status = client.enums.CampaignStatusEnum.ENABLED
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['status']))
req.operations.append(op)
client.get_service('CampaignService').mutate_campaigns(request=req)
Aggiornamento Budget
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 al giorno
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['amount_micros']))
req.operations.append(op)
client.get_service('CampaignBudgetService').mutate_campaign_budgets(request=req)
Aggiornamento Offerta Gruppo di Annunci
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)
Creazione Parola Chiave
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 = 'parola chiave esempio'
op.create.keyword.match_type = client.enums.KeywordMatchTypeEnum.EXACT
req.operations.append(op)
client.get_service('AdGroupCriterionService').mutate_ad_group_criteria(request=req)
Gestione degli Errori
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}')
Codici di errore comuni e loro significato:
| Codice Errore | Causa |
|---|---|
| `RESOURCE_NOT_FOUND` | Token di Accesso di Test contro un account di produzione (previsto) oppure risorsa effettivamente mancante |
| `REQUIRED` | Campo obbligatorio mancante (es. strategia di offerta nella creazione della campagna) |
| `UNRECOGNIZED_FIELD` | Campo di una versione API diversa o nome campo errato |
| `INVALID_ARGUMENT` | Il valore del campo non supera la validazione (es. budget negativo) |
| `PERMISSION_DENIED` | L'utente OAuth non ha accesso all'account target |
Livelli di Accesso API
L'accesso alla Google Ads API è a livelli, non binario:
| Livello | Provisioning | Lettura (Produzione) | Scrittura (Produzione) | Scrittura (Account di Test) | Limite Operazioni Giornaliere |
|---|---|---|---|---|---|
| **Accesso di Test** | Immediato da API Center MCC | ✅ | ❌ | ✅ | 15.000 |
| **Accesso Base** | Richiesta + revisione Compliance | ✅ | ✅ | ✅ | 15.000 |
| **Accesso Standard** | Soglia di spesa + revisione | ✅ | ✅ | ✅ | Illimitato |
L'Accesso di Test è sufficiente per lo sviluppo: autenticazione, query GAQL, test validate_only e lettura/scrittura completa su account di test Google Ads. Il passaggio all'Accesso Base è condizionato dalla revisione di compliance, non dalla spesa.
L'Accesso Standard rimuove il limite giornaliero di operazioni. Richiede uno storico di spesa gestita tramite il token oppure una richiesta separata.
Account di Test
Gli account di test sono account Google Ads gratuiti senza fatturazione. Accettano qualsiasi mutation. Per crearne uno: nell'MCC, vai a Account → + → Crea nuovo account → Account di test. Collegalo all'MCC e usalo come customer_id target durante lo sviluppo con Accesso di Test.
`validate_only` tra i Livelli di Accesso
validate_only=True funziona a tutti i livelli di accesso, ma l'Accesso di Test rifiuta comunque le mutation validate_only su account di produzione con RESOURCE_NOT_FOUND. Non è una limitazione di validate_only — è lo stesso controllo di accesso applicato a tutte le operazioni di scrittura indipendentemente dal flag validate_only.
Conversion Tracking: Lettura API vs Stato Funzionale
L'interfaccia Google Ads potrebbe mostrare "Configura il monitoraggio delle conversioni" come raccomandazione anche quando un'azione di conversione esiste ed è abilitata. L'API può confermare l'esistenza di un'azione di conversione (tipo WEBPAGE, categoria PURCHASE, stato ENABLED, include_in_conversions_metric: true), ma questo dimostra solo che l'oggetto azione esiste. Non dimostra che il sito web emetta l'evento AW-.../label corrispondente, che il tag Google sia caricato o che un segnale di conversione sia stato ricevuto.
Durante l'audit del conversion tracking, separa il controllo API (metadati dell'azione di conversione) dal controllo browser (scheda network, caricamento tag, attivazione eventi). L'API è la fonte di verità per la configurazione; il browser è la fonte di verità per l'esecuzione.
Il pattern per l'attribuzione dei ricavi lato backend si applica qui: se gli eventi di conversione lato client non sono affidabili, pianifica il caricamento delle conversioni lato server o offline tramite il ConversionUploadService della Google Ads API come fallback.
Versionamento API
La libreria Python google-ads viene distribuita con più versioni API. A giugno 2026, sono disponibili dalla v19 alla v22. Ogni versione aggiunge, depreca o rimuove campi GAQL e metodi di servizio.
Regole per la selezione della versione:
- Specifica la versione esplicitamente in
load_from_storage(version='v22'). Il default potrebbe essere obsoleto. - Controlla le note di rilascio della Google Ads API per le deprecazioni dei campi prima di aggiornare.
- Il metodo
search_streamè disponibile dalla v6+ ed è il percorso di lettura raccomandato. - Protobuf-plus (
use_proto_plus: true) è richiesto per la v12+. - Il pattern a oggetti request per le mutation (
mutate_campaigns(request=req)) ha sostituito lo stile a keyword argument nella v17+.
Sicurezza Locale
Tutti i file di credenziali devono rimanere fuori dal controllo versione:
~/.hermes/
google-ads.yaml # 0600 — dev token, client OAuth, refresh token
google_ads_token.json # 0600 — token OAuth, scope adwords
google_client_secret.json # 0600 — client ID + secret OAuth
Per il repository di lavoro, aggiungi un .gitignore:
google-ads.yaml
*_token*.json
*_secret*.json
Un file template con valori placeholder può essere committato per documentare la struttura prevista:
# google-ads.yaml.template — committa questo, compila localmente
developer_token: INSERISCI_DEV_TOKEN
client_id: INSERISCI_CLIENT_ID
client_secret: INSERISCI_CLIENT_SECRET
refresh_token: INSERISCI_REFRESH_TOKEN
login_customer_id: INSERISCI_MCC_ID
use_proto_plus: true
Campi Obbligatori per le Operazioni Comuni
| Operazione | Campi Obbligatori |
|---|---|
| Crea Campagna (Search) | `name`, `status`, `advertising_channel_type`, `campaign_budget`, `manual_cpc` o strategia di offerta |
| Crea Gruppo di Annunci (Search) | `name`, `status`, `type_`, `cpc_bid_micros`, `campaign` |
| Crea Parola Chiave | `ad_group`, `status`, `keyword.text`, `keyword.match_type` |
| Aggiorna Stato Campagna | `resource_name`, `status`, `update_mask` (paths: `['status']`) |
| Aggiorna Budget | `resource_name`, `amount_micros`, `update_mask` (paths: `['amount_micros']`) |
Un campo obbligatorio mancante restituisce REQUIRED con il nome del campo. Un update_mask mancante in un'operazione di aggiornamento viene ignorato silenziosamente — il campo non viene aggiornato e non viene restituito alcun errore.
Script di Test Completo
Un singolo script che legge tutti i tipi di entità ed esegue test di scrittura validate_only:
# google_ads_api_test.py — legge tutte le entità + test di scrittura validate_only
import warnings
warnings.filterwarnings('ignore')
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
from google.protobuf import field_mask_pb2
CFG = '/path/to/google-ads.yaml'
CID = '1305475941'
c = GoogleAdsClient.load_from_storage(CFG, version='v22')
gs = c.get_service('GoogleAdsService')
def gaql(query, label):
print(f'\n=== {label} ===')
n = 0
for batch in gs.search_stream(customer_id=CID, query=query):
for row in batch.results:
n += 1
print(f'{n} righe')
# Legge tutte le entità
gaql("SELECT customer.id, customer.descriptive_name FROM customer WHERE customer.id = ...", "CUSTOMER")
gaql("SELECT campaign.id, campaign.name, campaign.status, ... FROM campaign", "CAMPAGNE")
gaql("SELECT campaign_budget.id, campaign_budget.name, ... FROM campaign_budget", "BUDGET")
gaql("SELECT ad_group.id, ad_group.name, ... FROM ad_group", "GRUPPI DI ANNUNCI")
gaql("SELECT ad_group_criterion.keyword.text, ... FROM keyword_view WHERE ...", "PAROLE CHIAVE")
gaql("SELECT ad_group_ad.ad.id, ... FROM ad_group_ad", "ANNUNCI")
gaql("SELECT conversion_action.id, ... FROM conversion_action", "CONVERSIONI")
# Test di scrittura
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_CAMPAGNA'
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('Crea Campagna (validate_only)', test_create_campaign)
print('\nFatto.')
Versione eseguibile completa disponibile nella skill per le operazioni Google Ads API nel repository delle operazioni pubblicitarie.
Riferimenti
- Documentazione Google Ads API
- Libreria Python google-ads
- Riferimento GAQL
- OAuth 2.0 per App Desktop
- Creare un Assistente AI Specifico per Progetto via Telegram — stesso pattern OAuth applicato a uno scope Google diverso
- Architettura di Riferimento per un Layer Operativo AI Basato su Thread — il modello operativo in cui si inserisce l'automazione API
- Operazioni in Solitaria a Scala: Gestire Decine di Progetti con un Team Ridotto — perché l'automazione API sostituisce il lavoro manuale su UI a scala