# Webhooks entrantes

Un **webhook entrante** es un endpoint que Meteor expone para que un tercero le haga `POST`. Cuando llega un evento, Meteor puede **disparar una tarea** o **emitir un evento** interno. Necesita el scope `webhooks:manage`.

> Estos son webhooks **entrantes** (tercero → Meteor). Si lo que quieres es que **Meteor te avise** cuando pasa algo (`run.completed`, etc.), salta a [Eventos salientes](#eventos-salientes-meteor-tu-servidor) más abajo.

## Crear un webhook

```ts
const wh = await met.webhooks.create({
  name: 'Entrada de leads',
  binding_type: 'generic',        // 'task' ejecuta una tarea; 'generic' solo emite evento
  verifier_type: 'hmac_sha256',   // 'none' para sin firma
});

console.log(wh.url);      // la URL a la que apunta el tercero
console.log(wh.secret);   // ⚠️ se muestra UNA sola vez
```

Guarda el `secret` apenas lo recibes: **no se vuelve a mostrar**. Con él verificas que cada entrega vino realmente de tu integración.

## Verificar la firma

El tercero firma el cuerpo con el `secret` (HMAC-SHA256) y manda la firma en un header. Del lado de Meteor, la verificación es automática si el webhook es `hmac_sha256`.

## Rotar el secret

Si sospechas que se filtró:

```ts
const rotated = await met.webhooks.rotateSecret(wh.id);
console.log(rotated.secret);   // nuevo secret; invalida el anterior
```

## Ver entregas

```ts
const events = await met.webhooks.events(wh.id, { limit: 20 });
const sample = await met.webhooks.sample(wh.id);   // último payload recibido
```

`sample` es útil para configurar el mapeo de campos del payload.

## Conectar un flujo

Un webhook `generic` puede disparar un flujo cuando recibe un evento (`connectFlow` en la API). Así, un `POST` externo arranca una automatización completa en Meteor.

## Eventos salientes (Meteor → tu servidor)

Un **evento saliente** es Meteor haciéndote `POST` a *ti* cuando algo pasa en tu workspace — por ejemplo, cuando un Run termina. Registras un endpoint, eliges qué eventos quieres, y Meteor entrega cada uno **firmado con HMAC** para que puedas verificar que vino de Meteor.

### Registrar un endpoint

Registras el endpoint desde tu panel: **Desarrolladores → Actividad → Webhooks**. Eliges la URL de tu servidor y qué eventos quieres (o `*` para todos). Al crearlo, Meteor te muestra el **secreto de firma** (`whsec_...`) **una sola vez** — guárdalo, lo necesitas para verificar cada entrega.

### Catálogo de eventos

Son **17**. La columna `scope` es el permiso que la key debe portar para recibir ese evento: el mismo con el que leerías ese recurso por la API.

| Evento | Cuándo dispara | Scope |
|---|---|---|
| `run.completed` | Un Run terminó con éxito. | `runs:read` |
| `run.failed` | Un Run falló. | `runs:read` |
| `channel.run.completed` | El Met respondió un mensaje de canal. | `runs:read` |
| `channel.run.failed` | El Met falló al responder un mensaje de canal. | `runs:read` |
| `contact.created` | Se creó un contacto. | `contacts:read` |
| `contact.updated` | Se actualizó un contacto. | `contacts:read` |
| `contact.message.received` | Entró un mensaje de un contacto por un canal (WhatsApp, etc.). | `conversations:read` |
| `conversation.handoff` | Una conversación pasó a operador humano. | `conversations:read` |
| `task.completed` | Una tarea agéntica terminó. | `tasks:read` |
| `billing.threshold` | El gasto de Energía de una API key cruzó un umbral de su presupuesto (50/80/100%). | `billing:read` |
| `app.authorized` | Un workspace autorizó tu app OAuth (nuevo grant). | `integrations:read` |
| `app.revoked` | Un workspace revocó el acceso de tu app OAuth (grant revocado). | `integrations:read` |
| `snapshot.published` | Una plantilla propia fue aprobada y publicada al marketplace. | `snapshots:read` |
| `snapshot.install.completed` | La instalación de una plantilla terminó. | `snapshots:read` |
| `snapshot.install.failed` | La instalación de una plantilla falló. | `snapshots:read` |
| `conversion.sent` | Una conversión se emitió con éxito a Meta (CAPI). | `conversions:read` |
| `conversion.discarded` | Una conversión propuesta fue descartada por la compuerta (incluye la razón). | `conversions:read` |

`*` **no es un evento**: es el comodín «todos los del catálogo», y cubre también los que agreguemos después. Si prefieres enterarte de un evento nuevo antes de empezar a recibirlo, suscríbete a la lista explícita.

Tres cosas que ahorran una depuración:

- **`channel.run.*` no es `run.*`.** Un `run.*` es un Run de la API, con `id` consultable; un `channel.run.*` es el Met respondiendo un mensaje de canal (WhatsApp y demás) y no deja fila en Runs. Si escuchas solo `run.completed`, las respuestas de canal no te llegan nunca.
- **`app.authorized` y `app.revoked` se entregan al workspace que es dueño de la app** OAuth —el tuyo, el del developer—, no al que autoriza.
- **Las entregas de prueba llegan con `type: "ping"`**, que no está en el catálogo. Tu handler debería ignorar lo que no reconoce en vez de fallar.

Esta misma tabla la devuelve la API, siempre al día:

```ts
const { data } = await met.webhooks.subscriptions.eventTypes();
// [{ type: 'run.completed', description: '…', scope: 'runs:read' }, …]
```

### Verificar la firma

Cada entrega trae el header `X-Met-Signature: t=<ts>,v1=<hmac>`. **Nunca** proceses un evento sin verificarlo — usa el helper del SDK, que hace la verificación timing-safe y chequea el timestamp (anti-replay):

```ts
import Met from '@meteor.ia/sdk';
const met = new Met(process.env.MET_API_KEY!);

app.post('/webhooks/met', (req, res) => {
  let event;
  try {
    event = met.webhooks.constructEvent(req.rawBody, req.headers['x-met-signature'], WHSEC);
  } catch {
    return res.status(400).send('firma inválida');
  }
  if (event.type === 'run.completed') { /* ... */ }
  res.sendStatus(200);   // responde 2xx rápido; Meteor reintenta si no.
});
```

> Necesitas el **cuerpo crudo** (raw body) para verificar la firma — configura tu framework para no re-serializar el JSON antes de `constructEvent`.

### Reintentos

Si tu endpoint no responde `2xx`, Meteor reintenta con backoff exponencial (1 min → 5 min → 30 min → 2 h → 6 h → 24 h) hasta 6 intentos. Después marca la entrega como `failed`. El **log de intentos** por endpoint está en tu Workbench (sección Desarrolladores).
