Google Ads API obsługuje 10 000 operacji dziennie na konto klienta w trybie Basic Access i timeout maksymalnie 1 godzinę na zapytanie, wobec 30 minut CPU i maksymalnie 50 skryptów dla Google Ads Scripts (oficjalna dokumentacja limitów API). Na kontach obserwowanych w publicznych benchmarkach Google Ads, zespoły data przechodzące ze Scripts do API Python odzyskują 5–12 godzin tygodniowo na pipeline'ach raportowania i audytu, i odblokowują przypadki użycia niemożliwe po stronie Scripts (synchronizacja BigQuery, batch mutacje 5 000+ encji, dwukierunkowa integracja CRM).
Ten przewodnik to krok po kroku Python setup dla początkujących. Dokładnie ta komenda do wklejenia, snippet OAuth działający za pierwszym razem, pierwsze zapytanie GAQL, i publiczne repozytorium GitHub do sforkowania na start. Bez teorii marketingowej, bez „odkryj nieskończone możliwości API" — kod który działa. Wymagania wstępne: Python 3.9+, konto Google Ads, 30 minut. Jeśli znasz już Google Ads Scripts, najpierw przeczytaj nasz przewodnik po 10 gotowych skryptach Google Ads, który kładzie podstawy automatyzacji, które API będzie rozszerzać. Nasz kalkulator zmarnowanego budżetu szacuje EUR/miesiąc spalane przez broad bez wykluczeń lub nadmierny bounce LP.
Dlaczego Google Ads API, skoro Scripts już istnieje?
Google Ads Scripts jest potężny, ale ograniczony: maksymalnie 30 minut CPU na uruchomienie, maksymalnie 50 aktywnych skryptów na konto, JavaScript ES5 (bez pakietów npm), brak dostępu do zewnętrznych bibliotek naukowych (numpy, pandas, scikit-learn). Google Ads API to szczebel wyżej: Python, Java, Go, .NET lub Ruby do wyboru, możliwa integracja z dowolnym stosem danych (BigQuery, Snowflake, Airflow, dbt), operacje batch, async, skalowanie pionowe i poziome bez limitów po stronie Google.
Kryterium przejścia jest proste. Zostań przy Scripts, jeśli: monitorujesz 1 konto lub ograniczone MCC, Twoje automatyzacje mieszczą się w 30 minutach runtime, nie potrzebujesz zewnętrznych bibliotek Python, Twój zespół nie chce utrzymywać infrastruktury. Przejdź do API, jeśli: zarządzasz 10+ kontami równolegle, synchronizujesz z data warehouse (BigQuery, Snowflake), integrujesz CRM (HubSpot, Salesforce) dwukierunkowo, lub udostępniasz operacje Google Ads w wewnętrznym produkcie (dashboard, narzędzie automatyzacji).
Operacyjny sweet spot: używaj Scripts do monitorowania single-account i prostych automatyzacji (alert budżetowy, automatyczne wykluczenia negatywne), przejdź do API dla pipeline'ów danych, multi-account, integracji CRM. Na kontach obserwowanych w publicznych benchmarkach Google Ads, około 30–40% dojrzałych struktur (>500 tys. EUR/rok wydatku) łączy oba: Scripts do taktycznego codziennego, API do strategicznego tygodniowego i synchronizacji danych.
Często pojawia się pytanie na szkoleniach: „mam już 4–5 działających Scripts, czy muszę wszystko migrować?" Pragmatyczna odpowiedź brzmi: nie. Migracja do API jest opłacalna tylko jeśli zyskujesz na możliwościach (multi-account, data warehouse, biblioteki naukowe) lub tracisz czas na ograniczeniach Scripts (timeout 30 min, brak pandas). Dla konta średniej wielkości ze sprawnie działającymi Scripts, pozostawienie kodu na miejscu i dodanie API tylko dla nowych przypadków użycia to strategia minimalizująca ryzyko. Oba pipeline'y współistnieją bez konfliktu: Scripts wykonuje się po stronie Google, Twoje API działa po stronie infrastruktury, nie wchodzą sobie w drogę.
Inny często zapomniany arbitraż dotyczy całkowitego kosztu posiadania. Scripts jest darmowy po stronie infrastruktury (Google hostuje), ale wymaga JavaScript w ograniczonym sandboxie — czyli godzin dev na obejście ograniczeń. API wymaga Pythona, infrastruktury (Cloud Run, Lambda, EC2, lub proste cron na VPS), monitoringu, zarządzania secretami i zależności do utrzymania. Przez 12 miesięcy setup API Python dla 3–5 skryptów kosztuje typowo od 300 do 1 200 EUR infrastruktury cloud, plus 20–60 godzin dev/utrzymania. Powyżej 10 skryptów lub 20 kont do zarządzania, ROI wyraźnie przechyla się na stronę API.
Konfiguracja środowiska Python: OAuth2, credentials, biblioteka
Setup mieści się w 6 krokach: utworzenie projektu GCP, wygenerowanie credentials OAuth2, uzyskanie developer_token Google Ads, instalacja biblioteki, generowanie refresh_token, test z zapytaniem GAQL. Całość zajmuje 30 minut. Oto dokładna procedura.
Krok 1 — Projekt GCP i włączenie API
Na console.cloud.google.com, utwórz nowy projekt (dowolna nazwa, np. google-ads-api-prod). W APIs and Services > Library wyszukaj „Google Ads API" i kliknij Enable. API jest bezpłatne, ale wymaga jawnego włączenia per projekt GCP.
Krok 2 — Credentials OAuth2 (typ Desktop app)
W APIs and Services > Credentials kliknij Create Credentials > OAuth client ID. Typ: Desktop app. Nadaj opisową nazwę (np. google-ads-api-cli). Pobierz JSON, zachowaj client_id i client_secret. Obie wartości zasilą google-ads.yaml.
Krok 3 — Developer token w Google Ads
W Google Ads: Tools and Settings > API Center. Jeśli jeszcze nie masz, poproś o developer_token. Początkowy token jest w trybie Test (15 000 ops/dzień, tylko konta testowe). Aby przejść do Basic Access (10 000 ops/dzień, konta produkcyjne), złóż wniosek z opisem przypadku użycia — Google odpowiada w ciągu 1–5 dni roboczych. Na Standard Access (bez limitu) — 2–4 tygodnie przeglądu.
Krok 4 — Instalacja biblioteki i generowanie refresh_token
Instalacja oficjalnej biblioteki:
# Python 3.9+ wymagany
pip install google-ads
# Sprawdź wersję (24.0.0+ odpowiada API v17)
pip show google-ads
Aby wygenerować refresh_token, najprostszym sposobem jest użycie oficjalnego skryptu uwierzytelniania dostarczonego przez Google:
# Sklonuj oficjalne przykłady
git clone https://github.com/googleads/google-ads-python.git
cd google-ads-python/examples/authentication
# Uruchom skrypt generowania
python generate_user_credentials.py \
--client_id YOUR_CLIENT_ID \
--client_secret YOUR_CLIENT_SECRET
Skrypt otwiera stronę OAuth w przeglądarce. Zatwierdź dostęp do konta Google Ads. Skrypt wydrukuje refresh_token w formacie 1//0g...XXXXX. Skopiuj go natychmiast — wyświetla się tylko raz.
Krok 5 — Konfiguracja google-ads.yaml
Utwórz plik google-ads.yaml w katalogu głównym projektu:
# google-ads.yaml — NATYCHMIAST DODAJ DO .gitignore
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" # Nadrzędne MCC bez myślników
use_proto_plus: true
login_customer_id to ID Twojego nadrzędnego MCC bez myślników (np. 123-456-7890 staje się 1234567890). To konto, pod którym API uwierzytelnia każde zapytanie. Jeśli odpytujesz konto klienta tego MCC, podasz customer_id konta klienta w samym zapytaniu.
Ajoutez google-ads.yaml au .gitignore AVANT le premier commit. Un refresh_token leak sur GitHub public est detecte par les bots en moins de 30 minutes et peut etre utilise pour facturer du spend sur votre compte. En production, charger le YAML depuis un secret manager (AWS Secrets Manager, GCP Secret Manager, Vault) — jamais en plain text sur le serveur.
Krok 6 — Testowanie setupu za pomocą prostego zapytania
# test_setup.py
from google.ads.googleads.client import GoogleAdsClient
CUSTOMER_ID = "1112223333" # ID konta klienta (nie 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()
Uruchom python test_setup.py. Jeśli widzisz 5 wydrukowanych nazw kampanii — setup jest poprawny. Jeśli błąd INVALID_CUSTOMER_ID, sprawdź format (10 cyfr bez myślników). Jeśli błąd NOT_ADS_USER, refresh_token jest powiązany z kontem Google, które nie ma dostępu do podanego customer_id.
Pierwsze zapytanie GAQL: wydajność kampanii
GAQL (Google Ads Query Language) to język zapytań Google Ads API. Składnia zbliżona do SQL, ale z odrębnościami: brak jawnych JOIN (zasoby są zagnieżdżone), hierarchiczne pola (campaign.id, metrics.clicks, segments.date) i DURING dla zakresów dat zamiast WHERE date BETWEEN.
Oto kompletny skrypt pobierający wydajność kampanii ENABLED z ostatnich 30 dni, z impressions, clicks, cost, conversions, CTR, CPC, CPA: Nasz kalkulator CPA w 2 wejściach zwraca wartość + medianę dla Francji w Twoim segmencie.
# 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']}")
Trzy kluczowe kwestie dotyczące tego zapytania. Po pierwsze: koszty są w mikros (1 EUR = 1 000 000 mikros). Zawsze dziel przez 1_000_000, aby uzyskać EUR. Po drugie: metrics.ctr to float między 0 a 1, pomnóż przez 100, aby uzyskać procent. Po trzecie: klauzula DURING LAST_30_DAYS jest odpowiednikiem WHERE segments.date BETWEEN '2026-03-28' AND '2026-04-26', ale znacznie czytelniejsza. Lista stałych DURING: TODAY, YESTERDAY, LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, THIS_MONTH, LAST_MONTH itd. (pełna lista).
Trzy dodatkowe pułapki często spotykane na początku pracy z GAQL. *Pułapka 1: brak SELECT . API wymaga jawnego zadeklarowania każdego pola. Ręczne wymienianie 25 pól jest żmudne, ale celowe — Google chce ograniczyć przepustowość i zmusić reklamodawcę do świadomości tego, co konsumuje. Utrzymanie wielokrotnie używalnej stałej Python CAMPAIGN_FIELDS = [...] zapobiega przepisywaniu listy w każdym skrypcie. Pułapka 2: segments.date zawsze wprowadza row-fanning. Zapytanie bez segments.date agreguje dla całego okresu DURING; z segments.date otrzymujesz jeden wiersz na kampanię na dzień, czyli 30x więcej wierszy. Wybieraj świadomie w zależności od potrzeby (sumy okresu vs szereg czasowy). Pułapka 3: ORDER BY jest obowiązkowe dla spójnej paginacji. API automatycznie paginuje powyżej 10 000 wierszy; bez jawnego ORDER BY kolejność stron nie jest gwarantowana i ryzykujesz pominięcie encji podczas przetwarzania batch.
Aby testować inne zapytania GAQL interaktywnie bez Pythona, Google dostarcza GAQL Query Builder w oficjalnej dokumentacji — to najszybszy sposób na iterowanie struktury zapytania przed zakodowaniem. Praktyczna wskazówka: prototypuj zapytanie w query builderze, wklej do skryptu Python, a dopiero potem dodaj mapowanie do kolumn BigQuery lub pandas. Unikaj przepisywania zapytania trzy razy podczas debugowania, bo zapomniano o jakimś polu.
Mutacje: tworzenie, aktualizacja, wstrzymywanie kampanii
Mutacje to operacje zapisu API: tworzenie kampanii, modyfikacja budżetu, wstrzymywanie słowa kluczowego, dodawanie wykluczenia negatywnego. Przechodzą przez dedykowane serwisy (CampaignService, CampaignBudgetService, KeywordPlanService itp.) i używają wzorca operacja > mutacja > odpowiedź.
Oto skrypt wstrzymujący kampanię po jej 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")
# Budowa resource_name (obowiązkowy format)
resource_name = campaign_service.campaign_path(customer_id, campaign_id)
# Operacja: update
campaign_operation = client.get_type("CampaignOperation")
campaign = campaign_operation.update
campaign.resource_name = resource_name
campaign.status = client.enums.CampaignStatusEnum.PAUSED
# Field mask (określ co aktualizujesz)
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)
Kluczowy wzorzec dla WSZYSTKICH mutacji: resource_name + update_mask. resource_name identyfikuje encję (customers/{customer_id}/campaigns/{campaign_id}), update_mask określa, które pola modyfikujesz (bez niego API zwraca INVALID_FIELD_MASK). protobuf_helpers.field_mask(None, campaign._pb) automatycznie generuje maskę z zmodyfikowanych pól.
Aby utworzyć nową kampanię (Search Standard, dzienny budżet 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 # utwórz w PAUSED, włącz po przeglądzie
campaign.manual_cpc.enhanced_cpc_enabled = False
campaign.campaign_budget = budget_resource
# Ustawienia sieci
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}")
Dobre praktyki przy mutacjach:
- Zawsze twórz w PAUSED na początku, ręcznie włączaj po przeglądzie. Kampania utworzona jako ENABLED przez pomyłkę może spalić budżet w ciągu kilku godzin.
- Loguj resource_name zwrócony przez API dla identyfikowalności.
- Zawijaj w try/except GoogleAdsException systematycznie (zob. sekcja retry).
- Testuj na koncie testowym przed dotknięciem produkcji. API nie oferuje natywnego trybu dry-run (w przeciwieństwie do Scripts).
Obsługa błędów i retry logic w produkcji
Google Ads API może zwracać 3 kategorie błędów: przejściowe (RESOURCE_EXHAUSTED, DEADLINE_EXCEEDED, UNAVAILABLE) uzasadniające retry z backoff, błędy klienta (INVALID_ARGUMENT, NOT_FOUND, PERMISSION_DENIED) które nigdy nie powiodą się przy ponownej próbie, oraz rate limit (TOO_MANY_REQUESTS) wymagający oczekiwania na następne okno.
Oto wrapper wykładniczego retry do wklejenia we wszystkich skryptach produkcyjnych:
# 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 automatycznie ponawiający z exponential backoff
przy przejściowych błędach Google Ads API.
"""
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:
# Sprawdź czy błąd jest powtarzalny (zwłaszcza 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:
# Błąd klienta, nie powtarzalny
for error in e.failure.errors:
print(f"[ERROR] {error.error_code}: {error.message}")
raise
return None
return wrapper
return decorator
# Użycie
@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))
Wzorzec: exponential backoff (opóźnienie podwaja się przy każdym retry, ograniczone do 60 sekund) + klasyfikacja błędów (powtarzalne vs niepowtarzalne). Nigdy nie rób retry przy INVALID_ARGUMENT — to błąd w Twoim kodzie, nie problem sieciowy. Nigdy nie rób retry w nieskończoność — maksymalnie 3–5 prób, inaczej maszkujesz strukturalny problem (odwołany token, definitywnie wyczerpany limit).
Dla ustrukturyzowanego logowania, używaj standardowego logging Python z formatem JSON do ingestion w stosie obserwabilności (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)
# Użycie
logger.info("Pulled 247 campaigns", extra={"customer_id": CUSTOMER_ID})
Sur les comptes observés dans les benchmarks Google Ads publics, les pipelines API sans retry logic plantent en moyenne 2 a 4 fois par mois sur des erreurs transitoires reseau ou rate limit, contre 0 a 1 fois par trimestre avec un wrapper retry + backoff. C'est l'investissement de 30 lignes de code qui passe un script de "demo" a "prod-ready".
OAuth refresh flow: o czym się zapomina w produkcji
refresh_token Google Ads nie wygasa, o ile jest używany przynajmniej raz na 6 miesięcy. Może jednak zostać odwołany w kilku sytuacjach, które warto znać. Przypadek pierwszy: użytkownik Google, który wygenerował token, zmienia hasło lub MFA — wszystkie refresh_tokeny powiązane z tym kontem stają się nieważne. Przypadek drugi: Google wykrywa podejrzane zachowanie (token używany z 30 różnych IP w ciągu 1 godziny), automatycznie odwołuje i wysyła email bezpieczeństwa. Przypadek trzeci: przekroczenie limitu 50 jednocześnie aktywnych refresh_tokenów per klient OAuth powoduje ciche FIFO, które unieważnia najstarsze. Z tego powodu zaleca się dedykowane techniczne konto serwisowe dla API (nigdy osobiste konto Google pracownika), ze stabilnym hasłem i sprzętowym kluczem MFA.
Wzorzec retry przy błędzie auth musi różnić się od retry przy przejściowym błędzie sieciowym. Jeśli otrzymujesz UNAUTHENTICATED lub PERMISSION_DENIED związane z tokenem, NIGDY nie rób retry z backoff — token nie ożyje. Zamiast tego wywołaj alert (PagerDuty, Slack ops) i pozwól człowiekowi zregenerować refresh_token przez skrypt generate_user_credentials.py. Mylenie tych dwóch przypadków może kosztować godziny debugowania i utracone konwersje. Aby ograniczyć niespodzianki, monitoruj kondycję tokena prostym cronem: minimalne zapytanie GAQL (SELECT customer.id FROM customer LIMIT 1) co 6 godzin, alert przy błędzie auth.
Rate limiting: praktyczne rozumienie limitów
Limity API mają dwa poziomy: operacje na dzień (Test 15 000, Basic 10 000 na konto klienta, Standard oficjalnie bez limitu, ale throttle przy ok. 1 miliona/dzień) i zapytania na sekundę (miękki limit ok. 50 RPS utrzymywanych na klienta OAuth, poniżej API odpowiada bez błędu). Jedna operacja = jeden zmutowany wiersz, jedno pobranie lub jedna strona wyników GAQL. Zapytanie GAQL zwracające 5 000 wierszy liczy jako 1 operacja, nie 5 000.
Najczęstsza pułapka w produkcji: batch mutacje przekraczające 5 000 operacji w jednym wywołaniu. API zwraca wtedy RESOURCE_EXHAUSTED nie z powodu dziennego limitu, ale limitu per-call wynoszącego 5 000 operacji. Poprawny wzorzec to chunking list: jeśli masz 12 000 wykluczeń negatywnych do dodania, zrób 3 wywołania po 4 000 słów kluczowych zamiast jednego z 12 000. Chunking dodaje 5 linii kodu i eliminuje 90% błędów w operacjach bulk.
# Wzorzec chunkingu dla batch mutacji
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")
Dla nightly batch pipeline'ów, dodanie time.sleep(0.5) między chunkami wyrównuje obciążenie bez degradowania globalnej przepustowości. Na kontach obserwowanych w publicznych benchmarkach Google Ads, batch 50 000 mutacji w chunkach po 4k + sleep 500ms trwa ok. 8–10 minut wobec 4–5 minut w chunkach po 4k bez sleep — ale z wskaźnikiem błędów podzielonym przez 5. Kompromis czas/niezawodność jest wart sleep.
6 gotowych do sforkowania skryptów Python
Aby przyspieszyć start, publikujemy publiczne repozytorium GitHub github.com/steerads/google-ads-python-starter z 6 udokumentowanymi skryptami Python, gotowymi do sforkowania. Każdy skrypt jest autonomiczny, konfigurowany przez zmienne środowiskowe, z wbudowaną retry logic. Oto lista ze snippetem przykładowym dla każdego.
Skrypt 1 — Pull wydajności kampanii LAST_30_DAYS
Pobiera kompletne KPI (impressions, clicks, cost, conversions, ROAS) kampanii ENABLED, eksportuje do CSV lub wypycha do BigQuery. Częstotliwość: dziennie lub godzinowo. Zob. kompletny snippet w sekcji 3.
Skrypt 2 — Bulk aktualizacja budżetu kampanii
Modyfikuje dzienne budżety N kampanii w jednej operacji batch. Przydatne do miesięcznego rebalancingu budżetu lub automatycznych korekt sezonowych.
# 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]
Skrypt 3 — Dodawanie wykluczeń negatywnych z raportu zapytań wyszukiwania
Pobiera Search Term Performance Report z ostatnich 30 dni, identyfikuje zapytania z 0 konwersji przy ponad 15 kliknięciach, dodaje jako negatywy na poziomie kampanii. Odpowiednik skryptu 2 z naszego przewodnika Scripts 10 ready-to-copy, ale w API do obsługi 100+ kampanii w jednym uruchomieniu.
Skrypt 4 — Wstrzymywanie nieefektywnych słów kluczowych (CTR + CPA)
Identyfikuje słowa kluczowe z CTR poniżej 1% I CPA powyżej 2x docelowego CPA przez 30 dni, automatycznie je wstrzymuje z logiem przed działaniem. Wielowymiarowe kryterium niemożliwe po stronie Scripts (wymuszające iterację na AdsApp.keywords()), trywialne w GAQL.
Skrypt 5 — Eksport raportu do BigQuery (data warehousing)
Pobiera zagregowane metryki, transformuje z pandas, ładuje do BigQuery przez google-cloud-bigquery. To przypadek użycia, w którym API przewyższa Scripts: niemożność poprawnego połączenia Scripts z BigQuery, podczas gdy po stronie Python to 10 linii.
# bigquery_export.py
from google.cloud import bigquery
import pandas as pd
def export_to_bigquery(client, customer_id, dataset_id, table_id):
# 1. Pobierz dane GAQL
perf = pull_performance(client, customer_id)
df = pd.DataFrame(perf)
df["snapshot_date"] = pd.Timestamp.today().normalize()
# 2. Klient BigQuery
bq = bigquery.Client()
table_ref = f"{bq.project}.{dataset_id}.{table_id}"
# 3. Ładowanie z dołączaniem
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() # czekaj na zakończenie
print(f"Loaded {len(df)} rows to {table_ref}")
Skrypt 6 — Codzienny monitoring z alertami Slack
Łączy pull wydajności + sprawdzanie anomalii (spend, CPA, konwersje) + push Slack przez webhook, jeśli wykryto anomalię. Zindustrializowany odpowiednik codziennego cron job monitoringu.
# 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 avg 7d
if c.get("cost_eur", 0) > c.get("avg_cost_7d", 0) * 2:
alerts.append(f"Spend +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)
Repo github.com/steerads/google-ads-python-starter zawiera dla każdego skryptu: kompletny udokumentowany kod, README z krokami konfiguracji, plik .env.example dla zmiennych oraz requirements.txt z przypiętymi wersjami dla odtwarzalności. Dla automatyzacji cross-platform zob. też nasz przewodnik n8n Google Ads automation flows i nasz przewodnik MCP Google Ads + Claude Desktop do zarządzania Google Ads konwersacyjnie z Claude.
Kiedy przejść ze Scripts do API
Przejście ze Scripts do API nie jest systematycznym upgradeem — to wybór kompromisu. Oto 5 konkretnych wyzwalaczy obserwowanych w publicznych benchmarkach podczas audytu.
Wyzwalacz 1: multi-account (10+ kont do zarządzania równolegle). Scripts jest ze swej natury mono-kontowy (jeden skrypt przypisany do konta lub MCC). Powyżej 10 kont do synchronizacji, API Python z pętlą for na liście customer_ids staje się znacznie prostsze w utrzymaniu.
Wyzwalacz 2: data warehousing. Jeśli chcesz wypychać metryki do BigQuery, Snowflake, Redshift, API jest obowiązkowe. Scripts UrlFetchApp może technicznie wywołać REST API, ale uwierzytelnianie GCP, batching, ETL z pandas — to wymaga Pythona.
Wyzwalacz 3: dwukierunkowa integracja CRM. Do wypychania offline konwersji z Salesforce/HubSpot, Google Ads API obsługuje OfflineUserDataJobService wykonujące bezpieczny batch upload. Zob. nasz przewodnik conversion tracking Google Ads dla ram funkcjonalnych.
Wyzwalacz 4: zaawansowane potrzeby ML/statystyczne. Statystyczne wykrywanie anomalii (ARIMA, Prophet), segmentacja słów kluczowych (clustering), prognozowanie wydatku — wszystkie te zadania wymagają numpy/pandas/scikit-learn, niemożliwych w Scripts.
Wyzwalacz 5: wewnętrzny produkt. Budujesz dashboard lub wewnętrzne narzędzie wyświetlające/manipulujące danymi Google Ads. Koniecznie API — Scripts nie może udostępniać UI ani odpowiadać na zapytania HTTP.
Dla 5 odwrotnych przypadków (jedno konto, prosty monitoring, brak potrzeb danych, niestechniczna ekipa, ograniczona infrastruktura), Scripts pozostaje lepszy pod względem kosztów/utrzymania. Nasz przewodnik Microsoft Ads Scripts obejmuje odpowiednik po stronie Microsoft.
Dla kont, które chcą zindustrializować bez kodowania własnego stosu, nasz moduł Auto-optymalizacji obejmuje odpowiednik 6 powyższych skryptów w trybie managed: pull wydajności multi-account, wykrywanie anomalii, alerty Slack, synchronizacja BigQuery, bez ani jednej linii Pythona do utrzymania. Zob. też naszą checklistę audytu Google Ads dla bazy audytu, który powinien poprzedzać wszelką automatyzację, oraz nasz porównanie Zapier vs Make dla uzupełniających automatyzacji no-code.
Częste błędy do unikania na początku pracy z API
Pięć powtarzających się błędów spowalnia początkujących Python w Google Ads API. Każdy kosztuje średnio kilka godzin debugowania, których można uniknąć znając pułapkę z góry. Oto lista z diagnozą i bezpośrednią korektą.
1. Mylenie customer_id z login_customer_id. Diagnoza: skrypt zwraca INVALID_CUSTOMER_ID lub NOT_ADS_USER, choć wszystko wydaje się dobrze skonfigurowane. Korekta: login_customer_id (w YAML) = ID nadrzędnego MCC, pod którym uwierzytelnia się API, bez myślników. customer_id (w zapytaniu) = ID konta klienta, które chcesz odpytać, bez myślników. Jeśli odpytujesz bezpośrednio konto nieznajdujące się pod MCC, podaj jego ID w obu miejscach. Zawsze usuwaj myślniki z formatu 123-456-7890 -> 1234567890.
2. Zapomnienie update_mask przy mutacjach. Diagnoza: mutacja kończy się błędem MISSING_REQUIRED_FIELD: update_mask lub wszystkie pola encji są zastępowane ich domyślnymi (katastrofa w produkcji). Korekta: dla każdej operacji update generuj maskę przez protobuf_helpers.field_mask(None, entity._pb) po ustawieniu pól. Maska informuje API, które pola modyfikujesz; bez niej API albo odmawia, albo interpretuje jako „wszystkie pola mają wartości domyślne" i resetuje wszystko.
3. Pętla z indywidualnymi pulls zamiast batch. Diagnoza: skrypt pobierający 500 kampanii trwa 25 minut zamiast 30 sekund. Korekta: NIGDY nie rób for campaign_id in ids: ga_service.search(...) z zapytaniem na kampanię. Zamiast tego jedno zapytanie GAQL filtrujące WHERE campaign.id IN (...) lub pobierające wszystko i filtrujące po stronie Python. API nie jest karane za rozmiar zapytania, jest karane za liczbę wywołań.
4. Testowanie bezpośrednio w produkcji bez konta testowego. Diagnoza: błąd zaokrąglenia w kodzie mutacji budżetu przez pomyłkę redukuje wszystkie budżety do 1 EUR. Produkcja płonie. Korekta: utwórz testowe konto Google Ads (bezpłatne, bez powiązanej karty) dla wszystkich mutacji w dev. Token Test obejmuje wyłącznie konta testowe — co jest mimowolną, ale pożądaną ochroną. Przełącz na Basic Access dopiero po walidacji kodu w sandboxie dla 2–3 rzeczywistych przypadków użycia.
5. Ignorowanie paginacji GAQL powyżej 10 000 wierszy. Diagnoza: skrypt pobierający słowa kluczowe dużego konta kończy się błędem powyżej 10k wierszy lub zwraca niekompletne dane bez błędu. Korekta: używaj ga_service.search_stream(...), który automatycznie paginuje bez ładowania całego wyniku do pamięci. Dla pulls 50 000+ wierszy jest to obowiązkowe — search() ładuje wszystko do pamięci i zakończy się niepowodzeniem na maszynach z małą RAM.
Oficjalna dokumentacja: portal Google Ads API oraz oficjalne repozytorium google-ads-python zawierające dziesiątki przykładów utrzymywanych przez Google.
Źródła
Oficjalne źródła wykorzystane w tym przewodniku:
FAQ
Czy do korzystania z Google Ads API potrzebny jest developer token?
Tak, jest obowiązkowy. Developer token jest powiązany z kontem MCC (manager) i uzyskuje się go przez Tools and Settings > API Center w Google Ads. Początkowy token startuje w trybie Test (limit 15 000 operacji/dzień, tylko konta testowe), następnie Basic Access (10 000 operacji/dzień, konta produkcyjne) na żądanie, a potem Standard Access (bez limitu) po przeglądzie przez Google. Czas oczekiwania: 1–5 dni roboczych na Basic, 2–4 tygodnie na Standard z opisem przypadku użycia. Na początku token Test w pełni wystarczy: testowanie OAuth, pisanie pierwszych zapytań GAQL, debugowanie mutacji na koncie testowym. Przejście do Basic następuje dopiero po walidacji kodu w sandboxie.
Jaka jest różnica między google-ads-python (oficjalny) a googleads (legacy)?
google-ads-python to nowoczesna oficjalna biblioteka korzystająca z API REST/gRPC v17+ (wersje 2024–2026). googleads była biblioteką legacy korzystającą z SOAP API v201809 (przestarzałą od 2022 roku, całkowicie wyłączoną pod koniec 2023). Jeśli trafisz na kod googleads lub AdWords API na forach, jest on przestarzały. W 2026 roku używaj WYŁĄCZNIE google-ads-python (pip install google-ads), wersja 24.0.0 minimum, co odpowiada API v17. Biblioteka udostępnia klienta GoogleAdsClient inicjowanego z YAML lub zmiennych środowiskowych, z typowanymi serwisami (CampaignService, KeywordService, GoogleAdsService dla zapytań GAQL). Oficjalna dokumentacja na developers.google.com/google-ads/api/docs/client-libs/python.
Ile zapytań GAQL na sekundę obsługuje API?
Limity zależą od poziomu developer tokena. Test = 15 000 operacji/dzień łącznie, Basic = 10 000 operacji/dzień na konto klienta, Standard = brak oficjalnego limitu, ale throttling po stronie Google w razie nadużyć. Jedna operacja = jedno zapytanie GAQL lub jedna mutacja. Dla kont obserwowanych w publicznych benchmarkach Google Ads dominującym wzorcem jest 200–800 zapytań/dzień dla skryptu monitorującego dziennie konto o średniej wielkości. Godzinowy limit stawek wynosi 10 000 zapytań/godzinę maksymalnie na klienta OAuth. Po przekroczeniu API zwraca RESOURCE_EXHAUSTED, który należy obsłużyć exponential backoff (zob. sekcja retry logic). Dla skryptu wymagającego 5 000+ mutacji zaplanuj batch processing ze sleep między partiami.
Czy API może zarządzać Smart Bidding i Performance Max?
Tak, API udostępnia wszystkie funkcje Google Ads, w tym najnowsze (Performance Max, Smart Bidding, Demand Gen). Dla Performance Max interagujesz z CampaignService (campaignType=PERFORMANCE_MAX), AssetGroupService (grupy assetów PMax) i ConversionGoalService. Dla Smart Bidding — w polach bidding_strategy kampanii (TARGET_CPA, TARGET_ROAS, MAXIMIZE_CONVERSIONS). Ograniczenie: tworzenie kampanii PMax przez API wymaga wszystkich assetów równocześnie (zdjęcia, nagłówki, opisy, sitelinki), co czyni kod bardziej złożonym niż tworzenie Search. Na początku lepiej tworzyć/modyfikować kampanie Search lub Shopping i przejść do PMax po ustabilizowaniu pipeline. Zob. nasz przewodnik Performance Max w zakresie strategii asset group.
Jak zabezpieczyć credentials OAuth w produkcji?
Trzy zasady: NIGDY nie commitować google-ads.yaml ani refresh_tokenów do Gita (dodaj je do .gitignore), używaj secret managerów (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) do przechowywania w prod, i rotuj refresh_tokeny okresowo (zalecane maksimum 90 dni). Do lokalnego setupu deweloperskiego YAML w katalogu home jest OK. W produkcji ładuj YAML z secret managera podczas uruchamiania aplikacji. developer_token, client_id, client_secret mogą być w zmiennych środowiskowych; refresh_token musi być w ścisłym secret managerze. Jeśli refresh_token wycieknie (przypadkowy commit na GitHub), natychmiast go odwołaj przez Google Cloud Console > APIs and Services > Credentials, wygeneruj nowy i przejrzyj logi API w poszukiwaniu ewentualnych złośliwych wywołań.