# Conversiones para Meta y Google Ads

Cuando un anuncio trae a alguien que termina comprando, agendando o dejando sus datos, la plataforma de anuncios necesita saberlo para optimizar. Con `met.conversions` le reportas ese hecho desde tu sistema —el que sabe que el pago entró— y Met lo lleva a Meta (Conversions API) y a Google Ads.

**No emites directo.** Todo lo que reportas pasa por la **compuerta de conversiones**, la misma por la que pasan las que proponen el Met y los flujos. La compuerta revisa que haya un anuncio detrás, que el evento esté en la lista del negocio y que el hecho esté respaldado, y te dice qué decidió. Es lo que evita que una integración con un error infle los resultados de las campañas.

Hay dos caminos:

| Método | Para qué | Se identifica por |
|---|---|---|
| `conversions.report()` | Una conversión de un **contacto** que llegó por un anuncio de WhatsApp, Messenger o Instagram. | `contact_id` |
| `conversions.reportWeb()` | El espejo en servidor de un evento del **Pixel** de tu sitio. | `event_id` del Pixel y las cookies o datos del visitante |

## Antes de empezar

- Tu key necesita `conversions:write`. **No funciona con una key de test** (`403` `test_mode_restricted`): cada conversión aceptada es un evento real en la cuenta de anuncios.
- El negocio tiene que tener las conversiones encendidas en el panel, con su lista de eventos. Sin eso, todo sale `discarded` con `reason: "integration_inactive"`.
- Los dos métodos devuelven `201` aunque la compuerta descarte: **lee `decision`** siempre.

## Una conversión de un contacto

```ts
const r = await met.conversions.report({
  contact_id: 4821,
  event_name: 'Purchase',
  value: 250000,
  currency: 'COP',
  bucket: 'pedido-10293',
});

if (r.decision !== 'accepted') console.log('No salió:', r.decision, r.reason);
```
```python
r = met.conversions.report(
    contact_id=4821,
    event_name="Purchase",
    value=250000,
    currency="COP",
    bucket="pedido-10293",
)
if r["decision"] != "accepted":
    print("No salió:", r["decision"], r.get("reason"))
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/conversions \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":4821,"event_name":"Purchase","value":250000,"currency":"COP","bucket":"pedido-10293"}'
```

| Campo | Qué es |
|---|---|
| `contact_id` | El contacto del CRM (ver [CRM y contactos](contactos.html)). Tiene que haber llegado por un anuncio: si no, sale `no_attribution`. |
| `event_name` | Un evento de la lista del negocio. Para WhatsApp, Meta pide nombres de mensajería: `Purchase`, `LeadSubmitted`, `InitiateCheckout`… no los del Pixel web (`Lead`, `Contact`). |
| `value`, `currency` | Valor y moneda ISO 4217 de tres letras (`COP`, `USD`). Una compra los exige. |
| `evidence` | En qué te basas, en una frase («pagó la factura 10293»). Casi todos los eventos lo exigen. |
| `bucket` | Lo que hace única a la conversión: el id del pedido, de la cita. Sin él, la unidad es el contacto: un mismo contacto solo convierte una vez por evento. |

### Qué exige la compuerta

| Evento | Para que salga |
|---|---|
| `Purchase` | `value` mayor que cero y `currency` válida. |
| `LeadSubmitted`, `InitiateCheckout`, `AddToCart`, `Schedule`, `ReviewProvided`, `QualifiedLead` | `evidence`. |
| Cualquier otro de la lista | `evidence`. |

Además, un contacto no acepta más de 10 conversiones en 24 horas (el tope por defecto): es el freno para un flujo que se quedó en bucle.

## Una conversión web (Pixel)

Si tu sitio dispara el Pixel en el navegador, repórtalo también desde tu servidor con el **mismo** `event_id`: Meta junta los dos y no lo cuenta doble.

```ts
// En el navegador: fbq('track', 'Purchase', { value: 250000, currency: 'COP' }, { eventID: 'ord-10293' });
const r = await met.conversions.reportWeb({
  event_name: 'Purchase',
  event_id: 'ord-10293',
  fbp: req.cookies._fbp,
  fbc: req.cookies._fbc,
  email: cliente.email,
  event_source_url: 'https://mitienda.co/gracias',
  client_ip_address: req.ip,
  client_user_agent: req.headers['user-agent'],
  value: 250000,
  currency: 'COP',
});
```
```python
r = met.conversions.report_web(
    event_name="Purchase",
    event_id="ord-10293",
    fbp=request.cookies.get("_fbp"),
    email=cliente_email,
    client_ip_address=ip_del_visitante,
    client_user_agent=user_agent_del_visitante,
    value=250000,
    currency="COP",
)
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/conversions/web \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event_name":"Purchase","event_id":"ord-10293","fbp":"fb.1.1730000000.123","gclid":"Cj0KCQ…","value":250000,"currency":"COP"}'
```

- Hace falta al menos una forma de reconocer al visitante: `fbp`, `fbc`, `email`, `phone` o `external_id` para Meta; `gclid`, `gbraid`, `wbraid`, `email`, `phone` o `external_id` para Google Ads. Sin ninguna, sale `no_match_key`.
- `email`, `phone` y `external_id` viajan en claro hasta Met y salen de Met **convertidos en hash SHA-256**; `fbp`, `fbc` y los identificadores de clic de Google van tal cual.
- `client_ip_address` y `client_user_agent` son los del **visitante**, no los de tu servidor: mejoran la coincidencia.
- Este camino no pide `evidence`: el hecho ya pasó en el navegador.
- Si el visitante llegó por un anuncio de Google, pasa también `gclid` (o `gbraid` / `wbraid`), que sale de la URL de llegada.

## Qué responde

```json
{ "decision": "discarded", "reason": "uncorroborated" }
```

| `decision` | Qué pasó |
|---|---|
| `accepted` | Pasó la compuerta y va camino a la plataforma. `eventId` es la llave con la que viaja. |
| `pending_corroboration` | Queda esperando una confirmación del CRM. Por la API no pasa —tu sistema es la fuente del hecho—: le pasa a una compra o a un lead calificado que propone el Met o un flujo. |
| `duplicate` | Ya había salido una igual: mismo contacto, evento y `bucket`, o mismo `event_id` en la web. No se manda dos veces. |
| `discarded` | No sale. `reason` dice por qué. |

| `reason` | Qué hacer |
|---|---|
| `integration_inactive` | El negocio no tiene las conversiones encendidas. |
| `no_attribution` | El contacto no llegó por un anuncio (o su clic ya no sirve). |
| `not_in_taxonomy` | `event_name` no está en la lista del negocio. |
| `uncorroborated` | Faltó `evidence`, o `value` y `currency` en una compra. |
| `rate_limited` | El contacto ya tuvo 10 conversiones aceptadas en 24 horas. |
| `missing_waba` | El contacto es de WhatsApp y falta la cuenta de WhatsApp Business en la configuración. |
| `no_match_key` | Conversión web sin ninguna forma de reconocer al visitante. |

## Enterarte de la entrega

`accepted` dice que salió de la compuerta; la entrega a Meta llega después. Suscríbete a los eventos (scope `conversions:read`; ver [Webhooks](webhooks.html)):

| Evento | Cuándo |
|---|---|
| `conversion.sent` | Meta recibió la conversión. Trae `event_id`, `event_name` y el `fbtrace_id` de Meta. |
| `conversion.discarded` | La compuerta descartó una conversión, la hayas propuesto tú, el Met o un flujo. Trae `reason`. |

## Errores comunes

| Código | Qué pasó |
|---|---|
| `400` `validation_error` | Falta `contact_id` o `event_name` (`event_id` en la web), o un campo pasa su largo máximo. |
| `403` `missing_scope` | La key no trae `conversions:write`. |
| `403` `test_mode_restricted` | Una key `met_test_` no reporta conversiones. |
