Google Ads API : Configuration, Authentification, Lecture et Écriture
Un guide technique pour la mise en place d'un accès programmatique à Google Ads — de l'architecture au premier appel API. Couvre la configuration MCC, OAuth 2.0, les niveaux de Developer Token, la bibliothèque Python google-ads, les requêtes de lecture GAQL et le pattern d'écriture validate_only.
Architecture
Trois entités distinctes sont nécessaires avant qu'un seul appel API ne fonctionne :
| Entité | Rôle | Exemple |
|---|---|---|
| **Compte Manager (MCC)** | Émet le Developer Token | `login_customer_id` |
| **Projet Google Cloud** | Client OAuth 2.0 pour l'authentification | Client ID + Secret |
| **Compte Google Ads** | Cible de toutes les opérations API | `customer_id` |
Il ne s'agit pas du même compte. Un MCC peut posséder un Developer Token sans posséder le compte cible — il doit seulement être lié en tant que manager. Le projet Google Cloud peut être sous une identité Google différente de celle du MCC. Le compte cible est le compte annonceur où résident les campagnes.
Cette séparation est importante car l'API Center (où les Developer Tokens sont provisionnés) n'est disponible qu'à l'intérieur des comptes Manager. Un compte Google Ads autonome ne peut pas accéder à l'API Center, même s'il dispose d'utilisateurs administratifs.
L'en-tête d'appel API assemble ces trois identités plus un token Bearer OAuth :
developer-token: <from MCC API Center>
login-customer-id: <MCC numeric ID, no dashes>
customer-id: <target account numeric ID>
Authorization: Bearer <OAuth 2.0 access token>
Prérequis
- Un compte Google Ads Manager avec une configuration complète
- Le compte publicitaire cible lié au compte Manager et le lien accepté
- Un projet Google Cloud avec l'API Google Ads activée
- Python 3.9+ avec la bibliothèque
google-ads(pip install google-ads) - Un navigateur pour le flux de consentement OAuth unique
Étape 1 : Compte Manager et Developer Token
L'API Center se trouve sous Outils et paramètres → Configuration → Centre API dans un compte Manager. Si l'API Center est absent, le compte n'est soit pas un compte Manager, soit sa configuration est incomplète.
Depuis l'API Center, demandez un Developer Token. Définissez le type de compte sur Annonceur (et non Agence ou Tierce Partie) lorsque le token est destiné à vos propres comptes. Le token est émis immédiatement au niveau Accès Test.
L'accès Test permet :
L'erreur pour les écritures sur des comptes de production avec l'accès Test est RESOURCE_NOT_FOUND — et non PERMISSION_DENIED. Ceci prête à confusion car la ressource existe bel et bien et est accessible en lecture. L'erreur signifie que l'API refuse d'acheminer la mutation sous le niveau d'accès actuel.
Demande d'Accès de Base
Pour écrire sur des comptes de production, faites une demande d'Accès de Base depuis la même page de l'API Center. Le formulaire de demande comporte 12 questions. Pour un usage interne sur vos propres comptes, les réponses pertinentes sont :
| Question | Réponse |
|---|---|
| Q4 : Relation avec un représentant Google | Non |
| Q6 : Modèle économique | Décrivez votre propre cas d'usage (e-commerce, génération de leads, reporting interne) |
| Q8 : Qui aura accès | Utilisateurs internes — employés uniquement |
| Q9 : Outil développé par un tiers | Non (scripts/agents propres) |
| Q10 : API Conversion Tracking et Remarketing | Non (sauf si vous l'utilisez réellement) |
| Q11 : Types de campagnes | Search, Performance Max, Shopping (selon le cas) |
| Q12 : Capacités | Création de campagnes, Gestion de campagnes, Reporting |
Un document de conception (PDF) est requis pour la Q7. Il doit décrire l'architecture de l'API, le flux d'authentification, la stratégie de limitation de débit, la gestion des erreurs et le cas d'usage exclusivement interne. Restez factuel — l'équipe de conformité vérifie la cohérence avec le cas d'usage déclaré.
Délai de traitement en juin 2026 : environ 3 jours ouvrés, avec une note concernant un volume de demandes plus élevé que d'habitude.
Étape 2 : OAuth 2.0
Configuration du Projet Google Cloud
Dans la console Google Cloud, sous le projet concerné :
- Activez l'API Google Ads (API et services → Bibliothèque)
- Configurez l'écran de consentement OAuth :
https://www.googleapis.com/auth/adwordsLe type Application de bureau utilise http://localhost comme URI de redirection. C'est correct — le flux OAuth ouvre un navigateur, Google redirige vers localhost avec le code d'autorisation, et un serveur HTTP local le capture.
Génération du Refresh Token
Un script Python qui exécute un serveur HTTP local sur 127.0.0.1:0 (port aléatoire), affiche l'URL d'autorisation, attend une requête, extrait le paramètre code et l'échange contre des tokens :
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from google_auth_oauthlib.flow import Flow
class CallbackHandler(BaseHTTPRequestHandler):
code_value = None
def do_GET(self):
qs = parse_qs(urlparse(self.path).query)
CallbackHandler.code_value = (qs.get('code') or [None])[0]
self.send_response(200)
self.end_headers()
self.wfile.write(b'OAuth complete.')
def log_message(self, format, *args):
return
server = HTTPServer(('127.0.0.1', 0), CallbackHandler)
redirect_uri = f'http://127.0.0.1:{server.server_port}/'
flow = Flow.from_client_secrets_file(
'client_secret.json',
scopes=['https://www.googleapis.com/auth/adwords'],
redirect_uri=redirect_uri
)
auth_url, _ = flow.authorization_url(
access_type='offline',
prompt='consent'
)
print(auth_url)
server.handle_request()
flow.fetch_token(code=CallbackHandler.code_value)
Path('google_ads_token.json').write_text(flow.credentials.to_json())
Deux paramètres sont importants :
access_type='offline' — renvoie un refresh token, pas seulement un access tokenprompt='consent' — force un nouvel écran de consentement même si l'utilisateur a déjà autorisé ; nécessaire pour obtenir un nouveau refresh token si les scopes ont changéLe refresh token est permanent sauf s'il est révoqué, si l'utilisateur change son mot de passe, ou si le token reste inutilisé pendant 6 mois.
Piège des Scopes avec des Clients Existants
Si le client OAuth a été précédemment autorisé pour d'autres scopes (Gmail, Drive, Calendar), Google renvoie l'union des anciens et des nouveaux scopes. La bibliothèque google-auth-oauthlib rejette cette divergence de scopes par défaut. Définissez la variable d'environnement avant de charger le flux :
import os
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
Alternativement, omettez include_granted_scopes=true de l'URL d'autorisation, ce qui indique à Google de ne demander que le scope explicitement passé.
Étape 3 : `google-ads.yaml`
La bibliothèque lit la configuration depuis un fichier YAML :
developer_token: <value>
client_id: <value>
client_secret: <value>
refresh_token: <value>
login_customer_id: 1911599764
use_proto_plus: true
Règles de sécurité :
0600 (lecture/écriture propriétaire uniquement)login_customer_id est l'identifiant numérique du MCC sans tirets. Il indique à l'API quel Developer Token du compte manager utiliser. Le customer_id cible est passé au moment de l'appel, pas dans la configuration.
use_proto_plus: true active l'interface protobuf-plus, qui est requise pour les versions actuelles de l'API.
Étape 4 : Initialisation du 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')
Le paramètre version sélectionne explicitement la version de l'API. La bibliothèque google-ads est livrée avec plusieurs versions installées en parallèle (v19 à v22 en juin 2026). Spécifiez toujours la version — la valeur par défaut peut être en retard ou ne pas prendre en charge les champs plus récents.
Vérifiez la connectivité :
cs = client.get_service('CustomerService')
customers = cs.list_accessible_customers()
ids = [r.split('/')[-1] for r in customers.resource_names]
# Example output: ['1305475941', '3977581086', '1911599764']
Étape 5 : Opérations de Lecture (GAQL)
Google Ads utilise GAQL (Google Ads Query Language), un langage de type SQL. Toutes les opérations de lecture passent par GoogleAdsService.search_stream(), qui évite la pagination et est préféré à search() pour la plupart des requêtes.
Campagnes avec Métriques de Performance
SELECT
campaign.id,
campaign.name,
campaign.status,
campaign.advertising_channel_type,
campaign.serving_status,
campaign.bidding_strategy_type,
metrics.impressions,
metrics.clicks,
metrics.cost_micros,
metrics.conversions,
metrics.conversions_value,
metrics.ctr,
metrics.average_cpc
FROM campaign
ORDER BY campaign.id
Budgets de 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
Groupes d'Annonces
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
Mots-clés (par Impressions)
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
Annonces avec Statut des Règles
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
Actions de Conversion
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 d'Exécution 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
Étape 6 : Opérations d'Écriture avec `validate_only`
L'API v22 nécessite des objets de requête, pas des arguments nommés. Chaque service de mutation (CampaignService, AdGroupService, AdGroupCriterionService, CampaignBudgetService) suit le même 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 = 'Campaign Name'
op.create.status = client.enums.CampaignStatusEnum.PAUSED
op.create.advertising_channel_type = client.enums.AdvertisingChannelTypeEnum.SEARCH
op.create.campaign_budget = 'customers/1305475941/campaignBudgets/123456789'
op.create.manual_cpc.enhanced_cpc_enabled = False
req.operations.append(op) # .append(), not .add()
client.get_service('CampaignService').mutate_campaigns(request=req)
Différences clés par rapport aux versions antérieures de l'API :
validate_only est un champ de l'objet requête, pas un argument nommé de la méthode mutatereq.operations.append(op), et non req.operations.add(op)'customers/1305475941/campaigns/22479990461'), pas des valeurs de retour de méthodes helperPorte de Mutation en Trois Phases
Pour la sécurité en production, les mutations passent par trois portes :
Porte 1 — validate_only=True : L'API valide la structure de la requête, les champs obligatoires et les références de ressources. Aucun objet n'est créé. Exécutez ceci en premier pour chaque mutation.
Porte 2 — Création en Pause : Passez validate_only=False et créez l'objet avec status=PAUSED. Vérifiez le résultat avec une requête de lecture avant de continuer.
Porte 3 — Activation en Production : Un appel API séparé qui met à jour uniquement le champ status à ENABLED. Cela doit nécessiter une approbation explicite et ne pas être intégré dans la création.
# Gate 3: enable a previously created paused campaign
req = client.get_type('MutateCampaignsRequest')
req.customer_id = '1305475941'
req.validate_only = False
op = client.get_type('CampaignOperation')
op.update.resource_name = 'customers/1305475941/campaigns/22479990461'
op.update.status = client.enums.CampaignStatusEnum.ENABLED
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['status']))
req.operations.append(op)
client.get_service('CampaignService').mutate_campaigns(request=req)
Mise à Jour de 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 per day
op.update_mask.CopyFrom(field_mask_pb2.FieldMask(paths=['amount_micros']))
req.operations.append(op)
client.get_service('CampaignBudgetService').mutate_campaign_budgets(request=req)
Mise à Jour d'Enchère de Groupe d'Annonces
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)
Création de Mot-clé
req = client.get_type('MutateAdGroupCriteriaRequest')
req.customer_id = '1305475941'
req.validate_only = True
op = client.get_type('AdGroupCriterionOperation')
op.create.ad_group = 'customers/1305475941/adGroups/165827073530'
op.create.status = client.enums.AdGroupCriterionStatusEnum.PAUSED
op.create.keyword.text = 'example keyword'
op.create.keyword.match_type = client.enums.KeywordMatchTypeEnum.EXACT
req.operations.append(op)
client.get_service('AdGroupCriterionService').mutate_ad_group_criteria(request=req)
Gestion des Erreurs
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}')
Codes d'erreur courants et leur signification :
| Code d'erreur | Cause |
|---|---|
| `RESOURCE_NOT_FOUND` | Token d'accès Test sur un compte de production (attendu) ou ressource véritablement manquante |
| `REQUIRED` | Champ obligatoire manquant (ex. stratégie d'enchère à la création de campagne) |
| `UNRECOGNIZED_FIELD` | Champ d'une version d'API différente ou nom de champ incorrect |
| `INVALID_ARGUMENT` | La valeur du champ échoue la validation (ex. budget négatif) |
| `PERMISSION_DENIED` | L'utilisateur OAuth n'a pas accès au compte cible |
Niveaux d'Accès à l'API
L'accès à l'API Google Ads est par paliers, pas binaire :
| Niveau | Provisionnement | Lecture (Production) | Écriture (Production) | Écriture (Comptes Test) | Limite d'opérations quotidiennes |
|---|---|---|---|---|---|
| **Accès Test** | Immédiat depuis l'API Center du MCC | ✅ | ❌ | ✅ | 15 000 |
| **Accès de Base** | Demande + examen de conformité | ✅ | ✅ | ✅ | 15 000 |
| **Accès Standard** | Seuil de dépenses + examen | ✅ | ✅ | ✅ | Illimité |
L'accès Test est suffisant pour le développement : authentification, requêtes GAQL, tests validate_only et lecture/écriture complète sur les comptes de test Google Ads. Le passage à l'Accès de Base est conditionné par un examen de conformité, pas par les dépenses.
L'Accès Standard supprime le plafond d'opérations quotidiennes. Il nécessite soit un historique de dépenses gérées via le token, soit une demande séparée.
Comptes de Test
Les comptes de test sont des comptes Google Ads gratuits sans facturation. Ils acceptent toute mutation. Pour en créer un : dans le MCC, allez dans Comptes → + → Créer un compte → Compte de test. Liez-le au MCC et utilisez-le comme customer_id cible pendant le développement avec l'Accès Test.
`validate_only` selon les Niveaux d'Accès
validate_only=True fonctionne à tous les niveaux d'accès, mais l'Accès Test rejette tout de même les mutations validate_only sur les comptes de production avec RESOURCE_NOT_FOUND. Ce n'est pas une limitation de validate_only — c'est le même contrôle d'accès appliqué à toutes les opérations d'écriture, indépendamment du drapeau validate_only.
Suivi des Conversions : Lecture API vs. Statut Fonctionnel
L'interface Google Ads peut afficher « Configurer le suivi des conversions » comme recommandation même lorsqu'une action de conversion existe et est activée. L'API peut confirmer l'existence d'une action de conversion (type WEBPAGE, catégorie PURCHASE, statut ENABLED, include_in_conversions_metric: true), mais cela prouve seulement que l'objet d'action existe. Cela ne prouve pas que le site web émet l'événement AW-.../label correspondant, que le tag Google est chargé, ou qu'un signal de conversion a été reçu.
Lors de l'audit du suivi des conversions, séparez la vérification API (métadonnées de l'action de conversion) de la vérification navigateur (onglet réseau, chargement du tag, déclenchement d'événement). L'API est la source de vérité pour la configuration ; le navigateur est la source de vérité pour l'exécution.
Le pattern d'attribution de revenus côté serveur s'applique ici : si les événements de conversion côté client ne sont pas fiables, prévoyez des téléchargements de conversion côté serveur ou hors ligne via le ConversionUploadService de l'API Google Ads comme solution de secours.
Gestion des Versions de l'API
La bibliothèque Python google-ads est livrée avec plusieurs versions de l'API. En juin 2026, les versions v19 à v22 sont disponibles. Chaque version ajoute, déprécie ou supprime des champs GAQL et des méthodes de service.
Règles pour la sélection de version :
load_from_storage(version='v22'). La valeur par défaut peut être obsolète.search_stream est disponible à partir de la v6+ et constitue le chemin de lecture recommandé.use_proto_plus: true) est requis pour la v12+.mutate_campaigns(request=req)) a remplacé le style d'arguments nommés dans la v17+.Sécurité Locale
Tous les fichiers de credentials doivent rester en dehors du contrôle de version :
~/.hermes/
google-ads.yaml # 0600 — dev token, OAuth client, refresh token
google_ads_token.json # 0600 — OAuth tokens, adwords scope
google_client_secret.json # 0600 — OAuth client ID + secret
Pour le dépôt de travail, ajoutez un .gitignore :
google-ads.yaml
*_token*.json
*_secret*.json
Un fichier template avec des valeurs placeholder peut être versionné pour documenter la structure attendue :
# google-ads.yaml.template — commit this, fill locally
developer_token: INSERT_DEV_TOKEN
client_id: INSERT_CLIENT_ID
client_secret: INSERT_CLIENT_SECRET
refresh_token: INSERT_REFRESH_TOKEN
login_customer_id: INSERT_MCC_ID
use_proto_plus: true
Champs Requis pour les Opérations Courantes
| Opération | Champs Requis |
|---|---|
| Créer une Campagne (Search) | `name`, `status`, `advertising_channel_type`, `campaign_budget`, `manual_cpc` ou stratégie d'enchère |
| Créer un Groupe d'Annonces (Search) | `name`, `status`, `type_`, `cpc_bid_micros`, `campaign` |
| Créer un Mot-clé | `ad_group`, `status`, `keyword.text`, `keyword.match_type` |
| Mettre à jour le Statut d'une Campagne | `resource_name`, `status`, `update_mask` (paths: `['status']`) |
| Mettre à jour un Budget | `resource_name`, `amount_micros`, `update_mask` (paths: `['amount_micros']`) |
L'absence d'un champ requis renvoie REQUIRED avec le nom du champ. L'absence de update_mask sur une opération de mise à jour est silencieusement ignorée — le champ n'est pas mis à jour et aucune erreur n'est retournée.
Script de Test Complet
Un script unique qui lit tous les types d'entités et exécute des tests d'écriture validate_only :
# google_ads_api_test.py — read all entities + validate_only write tests
import warnings
warnings.filterwarnings('ignore')
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
from google.protobuf import field_mask_pb2
CFG = '/path/to/google-ads.yaml'
CID = '1305475941'
c = GoogleAdsClient.load_from_storage(CFG, version='v22')
gs = c.get_service('GoogleAdsService')
def gaql(query, label):
print(f'\n=== {label} ===')
n = 0
for batch in gs.search_stream(customer_id=CID, query=query):
for row in batch.results:
n += 1
print(f'{n} rows')
# Read all entities
gaql("SELECT customer.id, customer.descriptive_name FROM customer WHERE customer.id = ...", "CUSTOMER")
gaql("SELECT campaign.id, campaign.name, campaign.status, ... FROM campaign", "CAMPAIGNS")
gaql("SELECT campaign_budget.id, campaign_budget.name, ... FROM campaign_budget", "BUDGETS")
gaql("SELECT ad_group.id, ad_group.name, ... FROM ad_group", "AD GROUPS")
gaql("SELECT ad_group_criterion.keyword.text, ... FROM keyword_view WHERE ...", "KEYWORDS")
gaql("SELECT ad_group_ad.ad.id, ... FROM ad_group_ad", "ADS")
gaql("SELECT conversion_action.id, ... FROM conversion_action", "CONVERSIONS")
# Write tests
def test_write(name, fn):
print(f'\n--- {name} ---')
try:
fn()
print('PASS')
except GoogleAdsException as e:
for err in e.failure.errors:
print(f'{err.error_code}: {err.message}')
def test_create_campaign():
req = c.get_type('MutateCampaignsRequest')
req.customer_id = CID
req.validate_only = True
op = c.get_type('CampaignOperation')
op.create.name = 'TEST_CAMPAIGN'
op.create.status = c.enums.CampaignStatusEnum.PAUSED
op.create.advertising_channel_type = c.enums.AdvertisingChannelTypeEnum.SEARCH
op.create.campaign_budget = f'customers/{CID}/campaignBudgets/123456789'
op.create.manual_cpc.enhanced_cpc_enabled = False
req.operations.append(op)
c.get_service('CampaignService').mutate_campaigns(request=req)
test_write('Create Campaign (validate_only)', test_create_campaign)
print('\nDone.')
Version exécutable complète dans la compétence d'opérations Google Ads API du dépôt d'opérations publicitaires.