# Meta Conversions API (CAPI): implementación server-side

## Por qué CAPI es necesario post-iOS 14

iOS 14 introdujo App Tracking Transparency (ATT) e Intelligent Tracking
Prevention (ITP) de Safari. El impacto en el Pixel browser:

- **40-60% de eventos de conversión se pierden** en dispositivos Apple.
- Las cookies de terceros duran máximo 7 días en Safari (antes: 2 años).
- El atributo `_fbp` (identificador de usuario de Meta) expira en 7 días.

CAPI envía eventos directamente desde el servidor, sin depender del browser.
Los datos server-side tienen mayor confiabilidad porque no son bloqueados
por extensiones de ad-blocking ni restricciones del sistema operativo.

**Estrategia recomendada**: browser Pixel + CAPI en paralelo, con
deduplicación obligatoria vía `event_id`.

## Setup: credenciales necesarias

| Credencial | Dónde obtenerla | Dónde almacenarla |
|------------|----------------|-------------------|
| `PIXEL_ID` | Meta Business > Events Manager > tu Pixel | Variable de entorno del servidor |
| `CAPI_TOKEN` | Events Manager > Settings > Generate Access Token | Secret Manager / variable de entorno |

El token de acceso NO tiene fecha de expiración pero se puede revocar manualmente.
Rotar el token si se sospecha compromiso de seguridad.

## Hashing de PII: función de referencia

Meta requiere SHA-256 con normalización específica por tipo de dato:

```python
import hashlib

def hash_pii(valor: str) -> str:
    """
    Normaliza y hashea PII para Meta CAPI (SHA-256).
    - Email: minúsculas, sin espacios al inicio/fin
    - Teléfono: formato E.164 (+52XXXXXXXXXX), solo dígitos y +
    - Nombre/apellido: minúsculas, sin espacios al inicio/fin
    """
    return hashlib.sha256(valor.strip().lower().encode()).hexdigest()

def normalizar_telefono(telefono: str) -> str:
    """Convierte teléfono a formato E.164 para hashear."""
    # Conservar solo dígitos y el + inicial
    limpio = '+' + ''.join(c for c in telefono if c.isdigit())
    return limpio
```

## Endpoint server-side completo

```python
import hashlib
import httpx
import time
import os
from typing import Optional

PIXEL_ID = os.environ["META_PIXEL_ID"]
CAPI_TOKEN = os.environ["META_CAPI_TOKEN"]
CAPI_URL = f"https://graph.facebook.com/v18.0/{PIXEL_ID}/events"

def hash_pii(valor: str) -> str:
    return hashlib.sha256(valor.strip().lower().encode()).hexdigest()

async def enviar_evento_capi(
    evento: str,
    event_id: str,
    user_data: dict,
    custom_data: dict,
    source_url: str,
    test_event_code: Optional[str] = None,
) -> dict:
    """
    Envía un evento a Meta CAPI desde el servidor.

    Args:
        evento: Nombre del evento estándar Meta (Purchase, Lead, ViewContent, etc.)
        event_id: ID único para deduplicación con el Pixel browser. Debe ser el
                  mismo valor enviado en el dataLayer del browser.
        user_data: Datos del usuario. Se hashean internamente.
                   Claves esperadas: email, phone, fbc, fbp, ip, user_agent.
        custom_data: Datos del evento (value, currency, content_ids, etc.)
        source_url: URL donde ocurrió el evento.
        test_event_code: Solo en desarrollo/staging. Obtener en Events Manager.

    Returns:
        Respuesta de la API de Meta como dict.
    """
    user_data_hasheado = {}

    if email := user_data.get("email"):
        user_data_hasheado["em"] = [hash_pii(email)]

    if phone := user_data.get("phone"):
        user_data_hasheado["ph"] = [hash_pii(phone)]

    # fbc y fbp NO se hashean — son identificadores propios de Meta
    if fbc := user_data.get("fbc"):
        user_data_hasheado["fbc"] = fbc

    if fbp := user_data.get("fbp"):
        user_data_hasheado["fbp"] = fbp

    if ip := user_data.get("ip"):
        user_data_hasheado["client_ip_address"] = ip

    if user_agent := user_data.get("user_agent"):
        user_data_hasheado["client_user_agent"] = user_agent

    payload = {
        "data": [{
            "event_name": evento,
            "event_time": int(time.time()),
            "event_id": event_id,
            "event_source_url": source_url,
            "action_source": "website",
            "user_data": user_data_hasheado,
            "custom_data": custom_data,
        }],
    }

    if test_event_code:
        payload["test_event_code"] = test_event_code

    async with httpx.AsyncClient() as client:
        response = await client.post(
            CAPI_URL,
            params={"access_token": CAPI_TOKEN},
            json=payload,
            timeout=10.0,
        )
        response.raise_for_status()
        return response.json()
```

### Uso en endpoint de compra confirmada (FastAPI)

```python
from fastapi import Request

@router.post("/ordenes/{orden_id}/confirmar")
async def confirmar_orden(
    orden_id: str,
    request: Request,
    db: AsyncSession = Depends(get_db),
    current_user = Depends(get_current_user),
):
    orden = await procesar_pago(orden_id, db)

    # Leer cookies de Meta del browser
    fbc = request.cookies.get("_fbc")
    fbp = request.cookies.get("_fbp")

    # El event_id viene del frontend para deduplicación
    event_id = request.headers.get("X-Event-Id", f"purchase_{orden_id}_{int(time.time())}")

    await enviar_evento_capi(
        evento="Purchase",
        event_id=event_id,
        user_data={
            "email": current_user.email,
            "phone": current_user.phone,
            "fbc": fbc,
            "fbp": fbp,
            "ip": request.client.host,
            "user_agent": request.headers.get("User-Agent"),
        },
        custom_data={
            "value": float(orden.total),
            "currency": "MXN",
            "content_ids": [item.sku for item in orden.items],
            "content_type": "product",
            "num_items": len(orden.items),
        },
        source_url=str(request.url),
    )

    return {"status": "confirmada", "orden_id": str(orden.id)}
```

## Deduplicación: cómo funciona

Meta deduplica eventos cuando recibe dos eventos con el mismo `event_id`
dentro de una ventana de 48 horas. El proceso:

1. Browser (Pixel) dispara `Purchase` con `event_id = "purchase_ORD-001_1234"`
2. Servidor (CAPI) envía `Purchase` con el mismo `event_id`
3. Meta los empareja y cuenta solo una conversión

**Cómo pasar el event_id del browser al servidor:**

```javascript
// Frontend: guardar event_id en header de la request de confirmación
const eventId = `purchase_${transactionId}_${Date.now()}`;

// 1. Disparar en dataLayer (browser Pixel lo captura)
dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: 'purchase',
  event_id: eventId,
  ecommerce: { transaction_id: transactionId, ... }
});

// 2. Incluir en la request al backend
await fetch(`/api/ordenes/${transactionId}/confirmar`, {
  method: 'POST',
  headers: {
    'X-Event-Id': eventId,
    'Content-Type': 'application/json',
  },
});
```

## Cómo obtener las cookies _fbc y _fbp

| Cookie | Descripción | Cómo se crea |
|--------|-------------|--------------|
| `_fbp` | Identificador de browser de Meta | Meta Pixel la crea al cargar |
| `_fbc` | Identificador de click de anuncio | Meta la crea cuando el usuario llega desde un anuncio (parámetro `fbclid` en URL) |

Ambas deben leerse del request en el servidor y enviarse a CAPI sin hashear.
Si no están presentes, omitir el campo (no enviar cadena vacía).

## Tasa de coincidencia (Event Match Quality)

El score EMQ mide qué tan bien Meta puede emparejar eventos con usuarios:

| Score | Interpretación |
|-------|---------------|
| 8-10 | Excelente — atribución confiable |
| 6-7 | Bueno — mínimo recomendado |
| 4-5 | Aceptable — mejorar si es posible |
| < 4 | Bajo — revisar qué user_data se está enviando |

**Para maximizar el EMQ**: enviar siempre `em` + `ph` + `fbp` + `fbc` +
`client_ip_address` + `client_user_agent`. Cada campo adicional mejora el score.

## Test Events: validar antes de producción

1. En Meta Business > Events Manager > tu Pixel > Test Events.
2. Copiar el `test_event_code` (formato `TEST12345`).
3. Pasarlo al parámetro `test_event_code` del endpoint.
4. Ejecutar el flujo de compra en staging.
5. Verificar en Test Events que el evento llega con el payload correcto.
6. Confirmar que aparece como "Deduplicated" si también se disparó el Pixel.
7. **Remover** el `test_event_code` antes de pasar a producción.

## Eventos estándar de Meta disponibles

| Evento | Cuándo usarlo |
|--------|--------------|
| `Purchase` | Compra completada con valor monetario |
| `Lead` | Registro de formulario de contacto o demo |
| `ViewContent` | Vista de página de producto o servicio |
| `AddToCart` | Agregar producto al carrito |
| `InitiateCheckout` | Inicio del proceso de pago |
| `CompleteRegistration` | Registro de cuenta completado |
| `Search` | Búsqueda en el sitio |
| `Subscribe` | Suscripción a newsletter o servicio recurrente |
