Die Google Ads API unterstützt 10.000 Operationen pro Tag und Client-Account in Basic Access und einen max. 1-Stunden-Timeout pro Query, vs. 30 Minuten CPU und max. 50 Skripte für Google Ads Scripts (offizielle API-Quota-Dokumentation). In öffentlichen Google-Ads-Benchmarks beobachtete Konten zeigen, dass Data-Teams, die von Scripts auf API in Python wechseln, 5 bis 12 Stunden pro Woche bei Reporting- und Audit-Pipelines zurückgewinnen — und Use Cases freischalten, die auf der Scripts-Seite unmöglich sind (BigQuery-Sync, Batch-Mutations von 5.000+ Entities, bidirektionale CRM-Integration).
Dieser Guide ist ein Schritt-für-Schritt-Python-Setup für Einsteiger. Hier die exakten Befehle zum Einfügen, das OAuth-Snippet, das beim ersten Versuch funktioniert, die erste GAQL-Query und das öffentliche GitHub-Repo zum Forken. Keine Marketing-Theorie, kein „Entdecken Sie die unendlichen Möglichkeiten der API" — nur Code, der läuft. Voraussetzungen: Python 3.9+, ein Google-Ads-Account, 30 Minuten. Wenn Sie bereits mit Google Ads Scripts vertraut sind, lesen Sie zuerst unseren Guide zu 10 fertigen Google Ads Scripts, der die Automatisierungs-Grundlagen legt, die die API erweitert. Unser Wasted-Ad-Spend-Rechner schätzt die $/Monat, die durch Broad ohne Negatives oder exzessiven LP-Bounce verbrannt werden.
Warum die Google Ads API, wenn es Scripts schon gibt?
Google Ads Scripts ist mächtig, aber eingeschränkt: max. 30 Minuten CPU pro Run, max. 50 aktive Skripte pro Konto, JavaScript ES5 (keine npm-Packages), kein Zugriff auf externe wissenschaftliche Libraries (numpy, pandas, scikit-learn). Die Google Ads API ist die Stufe darüber: Python, Java, Go, .NET oder Ruby Ihrer Wahl, mögliche Integration mit jedem Data Stack (BigQuery, Snowflake, Airflow, dbt), Batch-Operationen, Async, vertikale und horizontale Skalierung ohne Limit auf Google-Seite.
Das Wechselkriterium ist einfach. Bleiben Sie bei Scripts, wenn: Sie 1 Konto oder ein eingeschränktes MCC monitoren, Ihre Automatisierungen in 30 Minuten Runtime passen, Sie keine externen Python-Libs brauchen, Ihr Team keine Infrastruktur pflegen will. Wechseln Sie zur API, wenn: Sie 10+ Konten parallel steuern, Sie mit einem Data Warehouse synchronisieren (BigQuery, Snowflake), Sie ein CRM (HubSpot, Salesforce) bidirektional integrieren oder Sie Google-Ads-Operationen in einem internen Produkt (Dashboard, Automatisierungs-Tool) exponieren.
Der operative Sweet Spot: Scripts für Single-Account-Monitoring und einfache Automatisierungen (Budget-Alerts, Auto-Negatives) nutzen, zur API wechseln für Data Pipelines, Multi-Account, CRM-Integration. In öffentlichen Google-Ads-Benchmarks beobachtete Konten zeigen: Etwa 30 bis 40 % der etablierten Strukturen (über $500k/Jahr Spend) kombinieren beides: Scripts für tägliches Taktisches, API für wöchentliches Strategisches und Daten-Syncs.
Eine häufige Frage in Trainings: „Ich habe schon 4-5 Skripte laufen, muss ich alles migrieren?" Die pragmatische Antwort lautet nein. Die Migration zur API ist nur profitabel, wenn Sie Capability gewinnen (Multi-Account, Data Warehouse, wissenschaftliche Libs) oder Sie an Scripts-Limitationen Zeit verlieren (30-Min-Timeout, kein pandas). Für ein Mid-Size-Konto mit gut laufenden Scripts ist es die risikominimierende Strategie, den Code in place zu lassen und die API nur für neue Use Cases zu ergänzen. Beide Pipelines koexistieren konfliktfrei: Scripts läuft auf Google-Seite, Ihre API auf Ihrer Infrastruktur — sie kollidieren nicht.
Der andere oft vergessene Trade-off betrifft die Total Cost of Ownership. Scripts ist auf der Infrastruktur-Seite kostenlos (Google hostet), erfordert aber JavaScript in einer eingeschränkten Sandbox — also Dev-Stunden, um Limitationen zu umgehen. Die API erfordert Python, Infrastruktur (Cloud Run, Lambda, EC2 oder einfacher VPS-Cron), Monitoring, Secrets-Management und zu pflegende Dependencies. Über 12 Monate kostet ein API-Python-Setup für 3-5 Skripte typischerweise zwischen $300 und $1.200 an Cloud-Infrastruktur, plus 20 bis 60 Stunden Dev/Wartung. Jenseits von 10 Skripten oder 20 zu steuernden Konten kippt der ROI klar zur API.
Python-Environment-Setup: OAuth2, Credentials, Library
Das Setup passt in 6 Schritte: GCP-Projekt erstellen, OAuth2-Credentials generieren, Google-Ads-developer_token holen, Library installieren, refresh_token generieren, mit GAQL-Query testen. Rechnen Sie mit 30 Minuten für alles. Hier die exakte Prozedur.
Schritt 1 — GCP-Projekt und API-Aktivierung
Auf console.cloud.google.com ein neues Projekt erstellen (beliebiger Name, z. B. google-ads-api-prod). Unter APIs und Dienste > Library „Google Ads API" suchen und auf Enable klicken. Die API ist kostenlos, erfordert aber explizite Aktivierung pro GCP-Projekt.
Schritt 2 — OAuth2-Credentials (Desktop-App-Typ)
Unter APIs und Dienste > Credentials auf Create Credentials > OAuth Client-ID klicken. Typ: Desktop App. Aussagekräftiger Name (z. B. google-ads-api-cli). JSON herunterladen, client_id und client_secret behalten. Diese beiden Werte speisen google-ads.yaml.
Schritt 3 — Developer Token auf Google-Ads-Seite
In Google Ads, Tools und Einstellungen > API Center. Falls noch nicht geschehen, ein developer_token anfragen. Das initiale Token ist im Test-Modus (15.000 Ops/Tag, nur Test-Accounts). Um zu Basic Access (10.000 Ops/Tag, Prod-Accounts) zu wechseln, einen Antrag mit Use-Case-Beschreibung einreichen — Google antwortet binnen 1 bis 5 Werktagen. Für Standard Access (unlimited) rechnen Sie mit 2 bis 4 Wochen Review.
Schritt 4 — Library installieren und refresh_token generieren
Die offizielle Library installieren:
# Python 3.9+ erforderlich
pip install google-ads
# Version verifizieren (24.0.0+ entspricht API v17)
pip show google-ads
Um den refresh_token zu generieren, ist der einfachste Weg, das offizielle, von Google bereitgestellte Auth-Skript zu nutzen:
# Offizielle Beispiele klonen
git clone https://github.com/googleads/google-ads-python.git
cd google-ads-python/examples/authentication
# Generierungs-Skript laufen lassen
python generate_user_credentials.py \
--client_id YOUR_CLIENT_ID \
--client_secret YOUR_CLIENT_SECRET
Das Skript öffnet eine OAuth-Seite in Ihrem Browser. Validieren Sie den Zugriff auf das Google-Ads-Konto. Das Skript gibt einen refresh_token im Format 1//0g...XXXXX aus. Kopieren Sie ihn sofort, er wird nur einmal angezeigt.
Schritt 5 — google-ads.yaml konfigurieren
Eine google-ads.yaml-Datei im Projekt-Root erstellen:
# google-ads.yaml — SOFORT in .gitignore AUFNEHMEN
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" # übergeordnetes MCC ohne Bindestriche
use_proto_plus: true
Die login_customer_id ist die ID Ihres übergeordneten MCC ohne Bindestriche (z. B. wird 123-456-7890 zu 1234567890). Es ist das Konto, unter dem die API jede Anfrage authentifiziert. Wenn Sie einen Client-Account dieses MCC abfragen, geben Sie die customer_id des Client-Accounts in der Anfrage selbst an.
google-ads.yaml zur .gitignore hinzufügen, BEVOR Sie zum ersten Mal committen. Ein geleakter refresh_token auf öffentlichem GitHub wird von Bots in unter 30 Minuten erkannt und kann genutzt werden, um Spend auf Ihrem Konto zu erzeugen. In Production die YAML aus einem Secret Manager (AWS Secrets Manager, GCP Secret Manager, Vault) laden — niemals als Plain Text auf dem Server.
Schritt 6 — Setup mit einer einfachen Query testen
# test_setup.py
from google.ads.googleads.client import GoogleAdsClient
CUSTOMER_ID = "1112223333" # Client-Account-ID (nicht das 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()
python test_setup.py ausführen. Wenn Sie 5 angezeigte Kampagnen-Namen sehen, ist das Setup gut. Bei INVALID_CUSTOMER_ID-Fehler Format prüfen (10 Ziffern ohne Bindestriche). Bei NOT_ADS_USER-Fehler ist der refresh_token an einen Google-Account gebunden, der keinen Zugriff auf die spezifizierte customer_id hat.
Erste GAQL-Query: Kampagnen-Performance
GAQL (Google Ads Query Language) ist die Abfragesprache der Google Ads API. SQL-ähnliche Syntax, aber mit Besonderheiten: kein expliziter JOIN (Ressourcen sind verschachtelt), hierarchisierte Felder (campaign.id, metrics.clicks, segments.date) und ein DURING für Datumsbereiche statt WHERE date BETWEEN.
Hier ein vollständiges Skript, das die Performance ENABLED-Kampagnen über die letzten 30 Tage zieht, mit Impressions, Klicks, Kosten, Conversions, CTR, CPC, CPA. Unser CPA-Rechner mit 2 Inputs liefert den Wert + den deutschen Median für Ihre Vertikale.
# 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']}")
Drei kritische Punkte zu dieser Query. Erstens: Kosten sind in Micros (1 USD = 1.000.000 Micros). Immer durch 1_000_000 teilen, um USD zu erhalten. Zweitens: metrics.ctr ist ein Float zwischen 0 und 1, mit 100 multiplizieren, um einen Prozentsatz zu erhalten. Drittens: Die Klausel DURING LAST_30_DAYS ist äquivalent zu WHERE segments.date BETWEEN '2026-03-28' AND '2026-04-26', aber viel lesbarer. Liste der DURING-Konstanten: TODAY, YESTERDAY, LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, THIS_MONTH, LAST_MONTH etc. (vollständige Liste).
Drei zusätzliche Fallen, die beim GAQL-Einstieg häufig auftreten. *Falle 1: Kein SELECT . Die API erfordert explizite Deklaration jedes Feldes. 25 Felder von Hand auflisten ist verbose, aber beabsichtigt — Google will Bandbreite begrenzen und den Werbetreibenden zwingen zu wissen, was er konsumiert. Eine wiederverwendbare Python-Konstante CAMPAIGN_FIELDS = [...] zu pflegen vermeidet das Neuschreiben der Liste in jedem Skript. Falle 2: segments.date führt immer zu Row-Fanning. Eine Query ohne segments.date aggregiert über die gesamte DURING-Periode; mit segments.date erhalten Sie eine Row pro Kampagne pro Tag, also 30× mehr Rows. Bewusst nach Bedarf wählen (Periodensummen vs. Zeitreihen). Falle 3: ORDER BY ist obligatorisch für konsistente Pagination. Die API paginiert automatisch jenseits von 10.000 Rows; ohne expliziten ORDER BY ist die Seitenreihenfolge nicht garantiert und Sie riskieren beim Batch-Processing Entities zu verpassen.
Um andere GAQL-Queries interaktiv ohne Python zu testen, stellt Google einen GAQL Query Builder in der offiziellen Dokumentation bereit — der schnellste Weg, an der Query-Struktur zu iterieren, bevor Sie sie coden. Praktischer Tipp: Query im Query Builder prototypen, ins Python-Skript einfügen und erst dann das Mapping auf Ihre BigQuery-Spalten oder pandas hinzufügen. Vermeiden Sie es, die Query dreimal neu zu schreiben, weil Sie beim Debuggen ein Feld vergessen haben.
Mutations: Kampagne erstellen, aktualisieren, pausieren
Mutations sind die Schreiboperationen der API: Kampagne erstellen, Budget modifizieren, Keyword pausieren, Negative hinzufügen. Sie laufen über dedizierte Services (CampaignService, CampaignBudgetService, KeywordPlanService etc.) und nutzen das Pattern operation > mutation > response.
Hier ein Skript, das eine Kampagne anhand ihrer ID pausiert:
# 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")
# resource_name bauen (verpflichtendes Format)
resource_name = campaign_service.campaign_path(customer_id, campaign_id)
# Operation: update
campaign_operation = client.get_type("CampaignOperation")
campaign = campaign_operation.update
campaign.resource_name = resource_name
campaign.status = client.enums.CampaignStatusEnum.PAUSED
# Field Mask (spezifizieren, was wir aktualisieren)
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)
Das kritische Pattern für ALLE Mutations: resource_name + update_mask. Der resource_name identifiziert die Entity (customers/{customer_id}/campaigns/{campaign_id}), die update_mask spezifiziert, welche Felder modifiziert werden (ohne sie liefert die API INVALID_FIELD_MASK). Der protobuf_helpers.field_mask(None, campaign._pb) generiert die Mask automatisch aus modifizierten Feldern.
Um eine neue Kampagne zu erstellen (Standard Search, Tagesbudget $100, 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_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 # als PAUSED erstellen, nach Review aktivieren
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 created: {budget_rn}")
campaign_rn = create_search_campaign(
client, CUSTOMER_ID, "Test API Campaign", budget_rn
)
print(f"Campaign created (PAUSED): {campaign_rn}")
Mutation Best Practices:
- Immer initial als PAUSED erstellen, nach Review manuell aktivieren. Eine versehentlich als ENABLED erstellte Kampagne kann das Budget in wenigen Stunden verbrauchen.
- Den von der API zurückgegebenen resource_name loggen für Nachverfolgbarkeit.
- Systematisch in try/except GoogleAdsException wrappen (siehe Retry-Sektion).
- Auf einem Test-Account testen, bevor Sie Prod anfassen. Die API bietet keinen nativen Dry-Run-Modus (anders als Scripts).
Error Handling und Retry-Logik in Production
Die Google Ads API kann 3 Fehlerkategorien zurückgeben: transient (RESOURCE_EXHAUSTED, DEADLINE_EXCEEDED, UNAVAILABLE), die einen Retry mit Backoff rechtfertigen, Client-Errors (INVALID_ARGUMENT, NOT_FOUND, PERMISSION_DENIED), die nie erfolgreich retried werden, und Rate Limit (TOO_MANY_REQUESTS), die ein Warten auf das nächste Window erfordern.
Hier ein exponentieller Retry-Wrapper zum Einfügen in alle Production-Skripte:
# 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):
"""
Dekorator, der bei transienten Google-Ads-API-Fehlern automatisch
mit Exponential Backoff retryt.
"""
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:
# Prüfen, ob der Fehler retryable ist (insbesondere Rate Limit)
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:
# Non-retryable Client-Error
for error in e.failure.errors:
print(f"[ERROR] {error.error_code}: {error.message}")
raise
return None
return wrapper
return decorator
# Verwendung
@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))
Das Pattern: Exponential Backoff (Delay verdoppelt sich bei jedem Retry, gedeckelt bei 60 Sekunden) + Fehler-Klassifizierung (retryable vs. non-retryable). Niemals bei INVALID_ARGUMENT retryen — das ist ein Bug in Ihrem Code, kein Netzwerkproblem. Niemals unbegrenzt retryen — max. 3 bis 5 Retries, sonst maskieren Sie ein strukturelles Problem (Token revoked, Quota dauerhaft erschöpft).
Für strukturiertes Logging verwenden Sie das Standard-Python-logging mit JSON-Format zur Ingestion in einem Observability-Stack (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)
# Verwendung
logger.info("Pulled 247 campaigns", extra={"customer_id": CUSTOMER_ID})
In öffentlichen Google-Ads-Benchmarks beobachtete Konten zeigen: API-Pipelines ohne Retry-Logik stürzen im Schnitt 2 bis 4 Mal pro Monat bei transienten Netzwerk- oder Rate-Limit-Fehlern ab, vs. 0 bis 1 Mal pro Quartal mit einem Retry-+-Backoff-Wrapper. Es ist die 30-Code-Zeilen-Investition, die ein Skript von „Demo" auf „Prod-ready" hebt.
OAuth-Refresh-Flow: was in Production vergessen wird
Der Google-Ads-refresh_token läuft nicht ab, solange er mindestens alle 6 Monate genutzt wird. Allerdings kann er in mehreren Fällen revoked werden, die es zu kennen lohnt. Erster Fall: Der Google-User, der das Token generiert hat, ändert sein Passwort oder MFA — alle an diesen Account gebundenen refresh_tokens werden ungültig. Zweiter Fall: Google erkennt verdächtiges Verhalten (Token aus 30 verschiedenen IPs in 1 Stunde verwendet, beispielsweise), revoked automatisch und sendet eine Sicherheits-Email. Dritter Fall: Das Überschreiten des Limits von 50 gleichzeitig aktiven refresh_tokens pro OAuth-Client triggert einen stillen FIFO, der die ältesten ungültig macht. Daher empfehlen wir einen dedizierten Technical Service Account für die API (niemals den persönlichen Google-Account eines Mitarbeiters), mit stabilem Passwort und Hardware-Key-MFA.
Das Retry-Pattern bei Auth-Errors muss sich vom Transient-Network-Retry unterscheiden. Wenn Sie UNAUTHENTICATED oder PERMISSION_DENIED mit Token-Bezug erhalten, NIEMALS mit Backoff retryen — das Token wird nicht wiederbelebt. Stattdessen einen Alert auslösen (PagerDuty, Slack Ops) und einen Menschen den refresh_token via generate_user_credentials.py-Skript regenerieren lassen. Diese beiden Fälle zu verwechseln kann Stunden Debug und verlorene Conversions kosten. Um Überraschungen zu begrenzen, monitoren Sie die Token-Health mit einem einfachen Cron: eine minimale GAQL-Query (SELECT customer.id FROM customer LIMIT 1) alle 6 Stunden, Alert bei Auth-Failure.
Rate Limiting: Quotas in der Praxis verstehen
API-Quotas sind auf zwei Ebenen strukturiert: Operationen pro Tag (Test 15.000, Basic 10.000 pro Client-Account, Standard offiziell unlimited, aber bei rund 1 Mio./Tag gethrottlet) und Requests pro Sekunde (Soft Limit bei rund 50 RPS sustained pro OAuth-Client, darunter liefert die API ohne Fehler). Eine Operation = eine mutierte Row, ein Get oder eine Page von GAQL-Resultaten. Eine GAQL-Query, die 5.000 Rows liefert, zählt als 1 Operation, nicht als 5.000.
Die häufigste Falle in Production: Batch-Mutations, die in einem einzelnen Call 5.000 Operationen überschreiten. Die API liefert dann RESOURCE_EXHAUSTED zurück — nicht wegen der Tagesquota, sondern wegen eines Per-Call-Limits von 5.000 Operationen. Das richtige Pattern ist, die Listen zu chunken: Wenn Sie 12.000 Negative Keywords hinzufügen müssen, machen Sie 3 Calls à 4.000 Keywords statt eines einzelnen Calls von 12.000. Chunking fügt 5 Code-Zeilen hinzu und vermeidet 90 % der Fehler in Bulk-Operationen.
# Chunking-Pattern für Batch-Mutations
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")
Für nächtliche Batch-Pipelines glättet ein time.sleep(0.5) zwischen Chunks die Last, ohne den Gesamtdurchsatz zu degradieren. In öffentlichen Google-Ads-Benchmarks beobachtete Konten zeigen: Ein Batch von 50.000 Mutations gechunkt bei 4k + 500 ms Sleep läuft in ungefähr 8-10 Minuten vs. 4-5 Minuten in 4k-Chunks ohne Sleep — aber mit einer Failure-Rate, die durch 5 geteilt wird. Der Zeit-/Reliability-Trade-off ist den Sleep wert.
6 fertige Python-Skripte zum Forken
Um den Start zu beschleunigen, publizieren wir ein öffentliches GitHub-Repo github.com/steerads/google-ads-python-starter mit 6 dokumentierten, fork-bereiten Python-Skripten. Jedes Skript ist standalone, über Env-Vars konfiguriert, mit eingebauter Retry-Logik. Hier die Liste mit einem Beispiel-Snippet je Skript.
Skript 1 — Kampagnen-Performance LAST_30_DAYS pullen
Zieht vollständige KPIs (Impressions, Klicks, Kosten, Conversions, ROAS) der ENABLED-Kampagnen, exportiert in CSV oder pusht in BigQuery. Frequenz: täglich oder stündlich. Vollständiges Snippet siehe Sektion 3.
Skript 2 — Kampagnenbudgets bulk aktualisieren
Modifiziert Tagesbudgets von N Kampagnen in einer einzelnen Batch-Operation. Nützlich für monatliches Budget-Rebalancing oder automatische saisonale Anpassungen.
# 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]
Skript 3 — Negative Keywords aus Search Query Report hinzufügen
Zieht den Search Term Performance Report über 30 Tage, identifiziert Queries mit 0 Conversion und über 15 Klicks und fügt sie als Negatives auf Kampagnenebene hinzu. Äquivalent zu Skript 2 unseres 10-Ready-to-Copy-Scripts-Guides, aber via API, um 100+ Kampagnen in einem einzelnen Run zu handhaben.
Skript 4 — Schwache Keywords pausieren (CTR + CPA)
Identifiziert Keywords mit CTR unter 1 % UND CPA über 2× Target-CPA in 30 Tagen, pausiert sie automatisch mit einem Pre-Action-Log. Multidimensionales Kriterium auf der Scripts-Seite unmöglich (was zwingt, über AdsApp.keywords() zu iterieren), in GAQL trivial.
Skript 5 — Report nach BigQuery exportieren (Data Warehousing)
Zieht aggregierte Metriken, transformiert mit pandas, lädt nach BigQuery via google-cloud-bigquery. Das ist der Use Case, in dem die API Scripts schlägt: Unmöglichkeit, Scripts sauber an BigQuery zu binden, während es auf der Python-Seite 10 Zeilen sind.
# bigquery_export.py
from google.cloud import bigquery
import pandas as pd
def export_to_bigquery(client, customer_id, dataset_id, table_id):
# 1. GAQL-Daten pullen
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. Mit Append laden
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() # auf Abschluss warten
print(f"Loaded {len(df)} rows to {table_ref}")
Skript 6 — Tägliches Monitoring mit Slack-Alerts
Kombiniert Pull Performance + Anomalie-Check (Spend, CPA, Conversions) + Slack-Push via Webhook bei erkannter Anomalie. Industrialisiertes Äquivalent eines täglichen Monitoring-Cron-Jobs.
# daily_monitoring.py
import requests
SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
def detect_anomalies(perf):
alerts = []
for c in perf:
# Spend > 2x Schnitt 7d
if c.get("cost_usd", 0) > c.get("avg_cost_7d", 0) * 2:
alerts.append(f"Spend +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 = "Google-Ads-Anomalien erkannt:\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)
Das Repo github.com/steerads/google-ads-python-starter enthält für jedes Skript: vollständig dokumentierten Code, eine README mit Schritt-für-Schritt-Setup, eine .env.example-Datei für Variablen und eine gepinnte requirements.txt für Reproduzierbarkeit. Für Cross-Platform-Automatisierung siehe auch unseren n8n-Google-Ads-Automation-Flows-Guide und unseren MCP-Google-Ads-+-Claude-Desktop-Guide, um Google Ads konversationell aus Claude zu steuern.
Wann von Scripts zur API wechseln
Der Wechsel von Scripts zur API ist kein systematisches Upgrade — er ist eine Trade-off-Wahl. Hier die 5 konkreten Trigger, die in öffentlichen Benchmarks bei Audits beobachtet wurden.
Trigger 1: Multi-Account (10+ parallel zu steuernde Konten). Scripts ist im Wesen Mono-Account (ein Skript an ein Konto oder ein MCC gebunden). Jenseits von 10 zu synchronisierenden Konten wird die Python-API mit einer For-Loop auf der customer_ids-Liste sehr viel einfacher zu pflegen.
Trigger 2: Data Warehousing. Wenn Sie Metriken nach BigQuery, Snowflake, Redshift pushen wollen, ist die API obligatorisch. Scripts UrlFetchApp kann zwar technisch eine REST-API aufrufen, aber GCP-Authentifizierung, Batching, ETL mit pandas — das erfordert Python.
Trigger 3: Bidirektionale CRM-Integration. Um Offline Conversions aus Salesforce/HubSpot zu pushen, unterstützt die Google Ads API OfflineUserDataJobService, der sicheren Batch-Upload macht. Siehe unseren Google-Ads-Conversion-Tracking-Guide für den funktionalen Rahmen.
Trigger 4: Fortgeschrittene ML-/Statistik-Bedürfnisse. Statistische Anomalie-Erkennung (ARIMA, Prophet), Keyword-Segmentierung (Clustering), Spend-Forecasting — all diese Aufgaben erfordern numpy/pandas/scikit-learn, in Scripts unmöglich.
Trigger 5: Internes Produkt. Sie bauen ein Dashboard oder internes Tool, das Google-Ads-Daten anzeigt/manipuliert. Notwendigerweise API — Scripts kann keine UI exponieren oder auf einen HTTP-Request antworten.
Für die 5 umgekehrten Fälle (Single-Account, einfaches Monitoring, keine Daten-Bedürfnisse, nicht-tech Team, eingeschränkte Infrastruktur) bleibt Scripts in Kosten/Wartung überlegen. Unser Microsoft-Ads-Scripts-Guide deckt das Äquivalent auf der Microsoft-Seite ab.
Für Konten, die ohne eigenes Stack-Coding industrialisieren wollen, deckt unser Auto-Optimization-Modul das Äquivalent der 6 obigen Skripte im Managed-Modus ab: Multi-Account-Pull-Performance, Anomalie-Erkennung, Slack-Alerts, BigQuery-Sync — ohne eine Zeile Python zu pflegen. Siehe auch unsere Google-Ads-Audit-Checkliste für die Audit-Basis, die jeder Automatisierung vorausgehen muss, und unseren Zapier-vs.-Make-Vergleich für komplementäre No-Code-Automatisierungen.
Häufige Fehler beim API-Einstieg, die es zu vermeiden gilt
Fünf wiederkehrende Fehler bremsen Python-Einsteiger bei der Google Ads API. Jeder kostet im Schnitt mehrere Stunden vermeidbares Debugging, wenn Sie die Falle nicht im Voraus kennen. Hier die Liste mit Diagnose und direkter Korrektur.
1. customer_id und login_customer_id verwechseln. Diagnose: Das Skript liefert INVALID_CUSTOMER_ID oder NOT_ADS_USER zurück, obwohl alles gut konfiguriert scheint. Korrektur: login_customer_id (in der YAML) = ID des übergeordneten MCC, unter dem die API authentifiziert, ohne Bindestriche. customer_id (in der Anfrage) = ID des Client-Accounts, den Sie abfragen wollen, ohne Bindestriche. Wenn Sie direkt einen Account abfragen, der nicht unter einem MCC liegt, geben Sie seine ID an beiden Stellen ein. Bindestriche aus dem Format 123-456-7890 immer entfernen -> 1234567890.
2. update_mask bei Mutations vergessen. Diagnose: Die Mutation schlägt fehl mit MISSING_REQUIRED_FIELD: update_mask, oder alle Entity-Felder werden durch ihre Defaults ersetzt (Katastrophe in Prod). Korrektur: Für jede update-Operation die Mask via protobuf_helpers.field_mask(None, entity._pb) generieren, nachdem Sie die Felder gesetzt haben. Die Mask deklariert der API, welche Felder Sie modifizieren; ohne sie verweigert die API entweder oder interpretiert sie als „alle Felder sind auf ihrem Default-Wert" und resettet alles.
3. In Individual-Pull statt in Batch loopen. Diagnose: Ein Skript, das 500 Kampagnen pullen sollte, braucht 25 Minuten statt 30 Sekunden. Korrektur: NIEMALS for campaign_id in ids: ga_service.search(...) mit einer Query pro Kampagne machen. Stattdessen eine einzelne GAQL-Query, die WHERE campaign.id IN (...) filtert oder alles zieht und auf der Python-Seite filtert. Die API wird nicht auf Query-Größe penalisiert, sondern auf die Anzahl der Calls.
4. Direkt in Prod ohne Test-Account testen. Diagnose: Ein Rundungs-Bug im Budget-Mutation-Code droppt versehentlich alle Budgets auf $1. Prod brennt. Korrektur: Einen Test-Google-Ads-Account erstellen (kostenlos, keine zugeordnete CC) für alle Dev-Mutations. Das Test-developer_token deckt nur Test-Accounts ab — was ein willkommener unfreiwilliger Schutz ist. Erst nach Validierung des Codes in der Sandbox auf 2-3 realen Use Cases zu Basic Access wechseln.
5. GAQL-Pagination jenseits von 10.000 Rows ignorieren. Diagnose: Ein Skript, das Keywords aus einem großen Konto pullt, stürzt jenseits von 10k Rows ab oder liefert ohne Fehler unvollständig. Korrektur: ga_service.search_stream(...) verwenden, das automatisch paginiert, ohne das gesamte Ergebnis im Speicher zu laden. Für 50.000+ Row-Pulls ist das obligatorisch — search() lädt alles in Memory und stürzt auf Low-RAM-Maschinen ab.
Für offizielle Dokumentation siehe das Google-Ads-API-Portal und das offizielle google-ads-python-Repo, das Dutzende von Google gepflegte Beispiele enthält.
Quellen
Offizielle Quellen für diesen Leitfaden:
FAQ
Brauchen Sie ein Developer Token, um die Google Ads API zu nutzen?
Ja, das ist obligatorisch. Das Developer Token ist an ein MCC- (Manager-)Konto gebunden und wird über Tools und Einstellungen > API Center auf der Google-Ads-Seite bezogen. Das initiale Token startet im Test-Modus (begrenzt auf 15.000 Operationen/Tag, nur Test-Accounts), dann Basic Access (10.000 Operationen/Tag, Prod-Accounts) auf Anfrage, dann Standard Access (unlimitiert) nach Google-Review. Rechnen Sie mit 1 bis 5 Werktagen für Basic, 2 bis 4 Wochen für Standard mit einer Akte, die Ihren Use Case detailliert. Zum Start reicht das Test-Token mehr als aus: OAuth testen, Ihre ersten GAQL-Queries schreiben, Mutations auf einem Test-Konto debuggen. Der Wechsel zu Basic erfolgt erst nach Sandbox-Code-Validierung.
Was ist der Unterschied zwischen google-ads-python (offiziell) und googleads (Legacy)?
google-ads-python ist die moderne offizielle Library, die die REST/gRPC-API v17+ (Versionen 2024-2026) konsumiert. googleads war die Legacy-Library, die die SOAP-API v201809 konsumierte (deprecated seit 2022, Ende 2023 vollständig abgeschaltet). Wenn Sie in Foren auf googleads- oder AdWords-API-Code stoßen, ist das obsolet. Für 2026 verwenden Sie AUSSCHLIESSLICH google-ads-python (pip install google-ads), Version 24.0.0 mindestens, was der API v17 entspricht. Die Library exponiert einen GoogleAdsClient, initialisiert aus YAML oder Env-Vars, mit typisierten Services (CampaignService, KeywordService, GoogleAdsService für GAQL-Queries). Offizielle Dokumentation auf developers.google.com/google-ads/api/docs/client-libs/python.
Wie viele GAQL-Queries pro Sekunde unterstützt die API?
Quotas hängen vom Tier Ihres Developer Tokens ab. Test = 15.000 Operationen/Tag insgesamt, Basic = 10.000 Operationen/Tag pro Client-Account, Standard = keine offizielle Quota, aber Google-seitiges Throttling bei Missbrauch. Eine Operation = eine GAQL-Query oder eine Mutation. In öffentlichen Google-Ads-Benchmarks beobachtete Konten zeigen das dominante Muster von 200 bis 800 Queries/Tag für ein tägliches Monitoring-Skript auf einem Mid-Size-Konto. Stündliche Rate Limit ist max. 10.000 Queries/Stunde pro OAuth-Client. Darüber hinaus liefert die API RESOURCE_EXHAUSTED, was mit Exponential Backoff behandelt werden muss (siehe Retry-Logik-Sektion). Für ein Skript, das 5.000+ Mutations laufen lassen muss, planen Sie Batch-Processing mit Sleep zwischen Batches ein.
Kann die API Smart Bidding und Performance Max handhaben?
Ja, die API exponiert alle Google-Ads-Features, inklusive der jüngsten (Performance Max, Smart Bidding, Demand Gen). Für Performance Max interagieren Sie mit CampaignService (campaignType=PERFORMANCE_MAX), AssetGroupService (PMax Asset Groups) und ConversionGoalService. Für Smart Bidding liegt das in den bidding_strategy-Feldern der Kampagnen (TARGET_CPA, TARGET_ROAS, MAXIMIZE_CONVERSIONS). Einschränkung: Eine PMax-Kampagne via API zu erstellen erfordert alle Assets parallel (Bilder, Headlines, Beschreibungen, Sitelinks), was den Code komplexer macht als Search-Erstellung. Zum Start bevorzugen Sie das Erstellen/Modifizieren von Search- oder Shopping-Kampagnen und wechseln zu PMax, sobald die Pipeline stabilisiert ist. Siehe unseren Performance-Max-Guide für Asset-Group-Strategie.
Wie sichern Sie OAuth-Credentials in Production?
Drei Prinzipien: Comitten Sie NIEMALS google-ads.yaml oder refresh_tokens nach Git (zur .gitignore hinzufügen), nutzen Sie Secret Manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) für Prod-Storage und rotieren Sie refresh_tokens periodisch (max. 90 Tage empfohlen). Für ein lokales Dev-Setup ist die YAML im Home-Verzeichnis OK. Für Prod laden Sie die YAML beim App-Start aus dem Secret Manager. developer_token, client_id, client_secret können in Env-Vars liegen; der refresh_token muss strikt im Secret Manager liegen. Falls ein refresh_token leakt (versehentlicher GitHub-Commit), revoken Sie ihn sofort über Google Cloud Console > APIs und Dienste > Credentials, generieren einen neuen und auditieren API-Logs, um potenziell bösartige Calls zu erkennen.