# Google Ads API — Referencia técnica

## Autenticación

### Tipos de credenciales

| Tipo | Cuándo usar | Configuración requerida |
|------|-------------|------------------------|
| OAuth2 con refresh token | Acceso en nombre de un usuario | `client_id`, `client_secret`, `refresh_token` |
| Service Account | Procesos server-side sin interacción humana | Archivo JSON de service account + acceso delegado en MCC |
| Developer Token | Obligatorio en todos los casos | Aprobado por Google en la cuenta MCC principal |

### Niveles del Developer Token

| Nivel | Quota diaria | Cuentas accesibles |
|-------|-------------|-------------------|
| Test | 15,000 ops/día | Solo cuentas de prueba |
| Basic | 1,000 ops/día | Todas las cuentas vinculadas al MCC |
| Standard | 10,000 ops/día | Requiere solicitud y aprobación de Google |

En producción con reportes diarios, **Standard es necesario** si se tienen
más de 50 cuentas o consultas de más de 30 días de datos.

### Configuración con diccionario Python

```python
from google.ads.googleads.client import GoogleAdsClient

config = {
    "developer_token": settings.GOOGLE_ADS_DEVELOPER_TOKEN,
    "client_id": settings.GOOGLE_ADS_CLIENT_ID,
    "client_secret": settings.GOOGLE_ADS_CLIENT_SECRET,
    "refresh_token": settings.GOOGLE_ADS_REFRESH_TOKEN,
    "login_customer_id": settings.GOOGLE_ADS_MCC_ID,  # ID del MCC, sin guiones
    "use_proto_plus": True,  # Objetos Python nativos en lugar de protobuf raw
}

client = GoogleAdsClient.load_from_dict(config)
```

### Configuración con archivo YAML (alternativa)

```yaml
# google-ads.yaml (NO subir a repositorio)
developer_token: "Tu_Developer_Token"
client_id: "client_id.apps.googleusercontent.com"
client_secret: "client_secret"
refresh_token: "1//0g..."
login_customer_id: "1234567890"
use_proto_plus: true
```

```python
client = GoogleAdsClient.load_from_storage("google-ads.yaml")
```

---

## GAQL — Google Ads Query Language

### Estructura básica

```sql
SELECT campo1, campo2, metrica1
FROM recurso_principal
WHERE condicion1 AND condicion2
ORDER BY metrica1 DESC
LIMIT 100
```

### Recursos principales

| Recurso | Qué contiene |
|---------|-------------|
| `campaign` | Campañas: nombre, status, tipo, presupuesto |
| `ad_group` | Grupos de anuncios dentro de campañas |
| `ad_group_ad` | Anuncios individuales con sus métricas |
| `keyword_view` | Keywords configuradas con Quality Score |
| `search_term_view` | Términos exactos que activaron anuncios |
| `geographic_view` | Rendimiento por ubicación geográfica |
| `audience_view` | Rendimiento por audiencia configurada |
| `shopping_performance_view` | Rendimiento de campañas Shopping |

### Segmentos comunes

Los segmentos dividen las métricas por dimensión. No todos son compatibles
con todos los recursos — la API retorna error si se combina incorrectamente.

```sql
-- Segmentar por fecha
SELECT campaign.name, metrics.clicks, segments.date
FROM campaign
WHERE segments.date DURING LAST_30_DAYS

-- Segmentar por dispositivo
SELECT campaign.name, metrics.clicks, segments.device
FROM campaign
WHERE segments.date DURING LAST_7_DAYS

-- Rangos de fecha explícitos
WHERE segments.date BETWEEN '2026-01-01' AND '2026-03-31'
```

### Períodos predefinidos útiles

| Constante GAQL | Equivalente |
|----------------|-------------|
| `LAST_7_DAYS` | Últimos 7 días |
| `LAST_30_DAYS` | Últimos 30 días |
| `LAST_MONTH` | Mes calendario anterior completo |
| `THIS_MONTH` | Mes calendario actual hasta hoy |
| `LAST_YEAR` | Año anterior completo |

### Filtros de status importantes

```sql
-- Solo campañas activas (ENABLED)
WHERE campaign.status = 'ENABLED'

-- Campañas activas y pausadas (excluye eliminadas)
WHERE campaign.status IN ('ENABLED', 'PAUSED')

-- Search terms de campañas activas únicamente
WHERE campaign.status = 'ENABLED'
    AND ad_group.status = 'ENABLED'
```

---

## Paginación y streaming

### `search` vs `search_stream`

| Método | Cuándo usar | Comportamiento |
|--------|-------------|----------------|
| `search` | Resultados esperados < 10,000 filas | Devuelve páginas de 10,000 resultados |
| `search_stream` | Resultados esperados > 10,000 filas | Stream de chunks, más eficiente en memoria |

```python
# Para datasets grandes (search terms de cuentas con alto volumen)
stream = ga_service.search_stream(customer_id=customer_id, query=query)
resultados = []
for batch in stream:
    for row in batch.results:
        resultados.append(procesar_fila(row))
```

---

## Batch operations con `MutateGoogleAdsRequest`

Para operaciones de escritura (agregar negative keywords, pausar campañas),
agrupar en un solo request reduce el consumo de quota.

```python
from google.ads.googleads.client import GoogleAdsClient

def agregar_negative_keywords(
    customer_id: str,
    campaign_id: str,
    terminos: list[str],
) -> None:
    """Agrega lista de negative keywords a una campaña en un solo request."""
    campaign_criterion_service = client.get_service("CampaignCriterionService")

    operaciones = []
    for termino in terminos:
        operacion = client.get_type("CampaignCriterionOperation")
        criterio = operacion.create
        criterio.campaign = f"customers/{customer_id}/campaigns/{campaign_id}"
        criterio.negative = True
        criterio.keyword.text = termino
        criterio.keyword.match_type = client.enums.KeywordMatchTypeEnum.BROAD
        operaciones.append(operacion)

    campaign_criterion_service.mutate_campaign_criteria(
        customer_id=customer_id,
        operations=operaciones,
    )
```

---

## Errores comunes y solución

| Error | Causa probable | Solución |
|-------|----------------|----------|
| `PERMISSION_DENIED` | `login_customer_id` incorrecto o sin acceso | Verificar que el MCC tiene acceso a la sub-cuenta |
| `QUOTA_ERROR` | Quota diaria agotada | Implementar caché, solicitar nivel Standard |
| `NOT_FOUND` | `customer_id` o `campaign_id` no existe | Verificar IDs sin guiones |
| `INVALID_CUSTOMER_ID` | `customer_id` con guiones | `customer_id.replace("-", "")` |
| `AUTHENTICATION_ERROR` | `refresh_token` vencido o revocado | Regenerar refresh token en Google OAuth Playground |
| `RESOURCE_EXHAUSTED` | Demasiados requests en poco tiempo | Implementar retry con backoff exponencial |

### Retry con backoff exponencial

```python
import time
from google.ads.googleads.errors import GoogleAdsException

def ejecutar_con_retry(fn, max_reintentos: int = 3):
    """Ejecuta una llamada a la API con retry y backoff exponencial."""
    for intento in range(max_reintentos):
        try:
            return fn()
        except GoogleAdsException as ex:
            es_quota = any(
                "QUOTA" in str(e.error_code) for e in ex.failure.errors
            )
            if es_quota and intento < max_reintentos - 1:
                espera = 2 ** intento  # 1s, 2s, 4s
                time.sleep(espera)
                continue
            raise
```
