# Eventos

> Qué trae cada evento de webhook, con ejemplos reales.

Todos los eventos llegan con la misma envoltura. Lo que cambia es `data`.

```json
{
  "id": "<id de la entrega>",
  "event": "<nombre del evento>",
  "createdAt": "2026-09-24T17:31:08.412Z",
  "data": { }
}
```

## message.received

Un cliente te escribió. Llega por cada mensaje, de WhatsApp o de Instagram. Las reacciones (👍, ❤️) no lo disparan.

<Tabs>
  <Tab title="WhatsApp">
    ```json
    {
      "id": "01a0d4a1-3b4c-7d5e-8f60-7a8b9c0d1e2f",
      "event": "message.received",
      "createdAt": "2026-09-24T17:31:08.412Z",
      "data": {
        "id": "01a0d4a1-3a10-7b21-9c32-4d5e6f7a8b9c",
        "wamid": "wamid.HBgNNTQ5MTE1NTU1MDEwMRUCABIYFjNFQjBDMEY2RTc1QjQ4QjM1QTI0AA==",
        "conversationId": "01a0d456-e5a1-7b2c-8d3e-4f5a6b7c8d9e",
        "phoneNumberId": "109876543210987",
        "contact": {
          "id": "01a0d456-e419-7c69-a35f-38a00ae65b81",
          "phone": "5491155550101",
          "name": "Marta Gómez"
        },
        "type": "text",
        "text": "Hola, ¿tienen envío a Rosario?",
        "receivedAt": "2026-09-24T17:31:07.000Z"
      }
    }
    ```
  </Tab>
  <Tab title="Instagram">
    ```json
    {
      "id": "01a0d4b2-1c2d-7e3f-8a4b-5c6d7e8f9a0b",
      "event": "message.received",
      "createdAt": "2026-09-24T19:05:12.880Z",
      "data": {
        "id": "01a0d4b2-1a00-7b11-8c22-3d4e5f6a7b8c",
        "channel": "instagram",
        "conversationId": "01a0d4b1-9f8e-7d6c-8b5a-4f3e2d1c0b9a",
        "instagramAccountId": "17841400000000000",
        "contact": {
          "id": "01a0d4b1-9e00-7a11-8b22-3c4d5e6f7a8b",
          "instagram": "vale.rodriguez",
          "name": "Valentina Rodríguez"
        },
        "type": "text",
        "text": "¿Tienen la taza en color verde?",
        "receivedAt": "2026-09-24T19:05:11.000Z"
      }
    }
    ```
  </Tab>
</Tabs>

<ResponseField name="data.id" type="string">Id del mensaje en MINDO.</ResponseField>
<ResponseField name="data.wamid" type="string">Id del mensaje en WhatsApp. No viene en Instagram.</ResponseField>
<ResponseField name="data.channel" type="string">Solo en Instagram: `"instagram"`.</ResponseField>
<ResponseField name="data.conversationId" type="string">La conversación en el chat de MINDO.</ResponseField>
<ResponseField name="data.phoneNumberId" type="string">Tu número de WhatsApp que recibió el mensaje (útil si tienes varios).</ResponseField>
<ResponseField name="data.contact" type="object">
  `id`, `name` (el nombre de perfil) y `phone` (WhatsApp) o `instagram` (usuario de Instagram).
</ResponseField>
<ResponseField name="data.type" type="string">
  El tipo de mensaje: `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contacts`, `button` (tocó un botón de una plantilla), `interactive` (eligió una opción), entre otros.
</ResponseField>
<ResponseField name="data.text" type="string | null">
  El texto del mensaje. En adjuntos, el pie de foto o una descripción (por ejemplo “📷 Imagen”); en botones, el texto del botón.
</ResponseField>
<ResponseField name="data.receivedAt" type="string">Cuándo lo mandó el cliente.</ResponseField>

<Tip>
  Cada mensaje entrante abre (o renueva) la [ventana de 24 horas](/conceptos/ventana-24-horas): durante ese tiempo puedes responder con `text` libre por la API.
</Tip>

## message.status

Cambió el estado de un mensaje que mandaste, ya sea por la API, desde el chat o en una campaña.

```json
{
  "id": "01a0d4a2-8c9d-7e0f-8a1b-2c3d4e5f6a7b",
  "event": "message.status",
  "createdAt": "2026-09-24T17:24:36.615Z",
  "data": {
    "wamid": "wamid.HBgMNTczMDAxMjM0NTY3FQIAERgSN0VENTc4NDFGRDk0NEVEQkEA",
    "status": "read",
    "conversationId": "01a0d472-90ae-7187-9bb8-9f34c8d023c1",
    "contactId": "01a0d467-35b0-79f1-a6b6-1f834c8325eb",
    "phoneNumberId": "109876543210987",
    "campaignId": null,
    "errorCode": null
  }
}
```

<ResponseField name="data.wamid" type="string">
  El id de WhatsApp del mensaje: el mismo `wamid` que te devolvió `POST /messages`. Úsalo para encontrar el mensaje en tu sistema.
</ResponseField>
<ResponseField name="data.status" type="string">`sent`, `delivered`, `read` o `failed`.</ResponseField>
<ResponseField name="data.campaignId" type="string | null">Si el mensaje salió de una campaña, su id.</ResponseField>
<ResponseField name="data.errorCode" type="number | null">
  Con `failed`, el código de Meta. Por ejemplo `131026` (no se pudo entregar). Mira la tabla en [Errores](/api/errores#mensajes-que-se-aceptan-pero-despues-fallan).
</ResponseField>

<Note>
  El estado solo avanza: `sent` → `delivered` → `read`. Si WhatsApp nos informa varios cambios juntos, te mandamos solo el más avanzado, así que puedes recibir `read` sin haber recibido `delivered`. Nunca vas a recibir un estado “hacia atrás”.
</Note>

<Warning>
  Si mandas campañas grandes, este evento llega **una o más veces por cada destinatario**. Suscríbete solo si lo vas a usar, y asegúrate de que tu servidor aguante el volumen.
</Warning>

## contact.created

Se sumó un contacto nuevo a tu cuenta.

```json
{
  "id": "01a0d4a3-1d2e-7f30-8a41-5b6c7d8e9f0a",
  "event": "contact.created",
  "createdAt": "2026-09-24T17:12:54.041Z",
  "data": {
    "id": "01a0d467-e634-7f7f-b98b-bdb48431733e",
    "phone": "525512345678",
    "name": "Ximena Robles",
    "source": "api"
  }
}
```

<ResponseField name="data.source" type="string">
  De dónde vino:
  - `inbound`: te escribió por WhatsApp por primera vez.
  - `instagram`: te escribió por Instagram por primera vez (en lugar de `phone` trae `instagram`, el usuario).
  - `api`: lo creaste con [`POST /contacts`](/api-reference/contactos/crear-o-actualizar).
</ResponseField>

<Note>
  Los contactos que sumas desde la app (importando una planilla o a mano) y los que se crean al mandar un mensaje con `POST /messages` a un teléfono nuevo **no** disparan este evento.
</Note>

## contact.opted_out

Un contacto pidió no recibir más mensajes: tocó el botón de baja de una plantilla o escribió algo como “baja”, “stop”, “no recibir más” o “darme de baja”.

```json
{
  "id": "01a0d4a4-5e6f-7a80-9b1c-2d3e4f5a6b7c",
  "event": "contact.opted_out",
  "createdAt": "2026-09-24T18:02:11.004Z",
  "data": {
    "id": "01a0d456-e44f-787d-a8e7-dc7cb7b0ddb1",
    "phone": "5491155550102",
    "at": "2026-09-24T18:02:10.000Z"
  }
}
```

MINDO deja a ese contacto afuera de las campañas automáticamente. Si tienes otros sistemas que le mandan mensajes (tu CRM, tu tienda), usa este evento para marcarlo también ahí. Más en [Bajas y consentimiento](/conceptos/bajas-y-consentimiento).

<Warning>
  La API **no** bloquea los envíos a contactos dados de baja: `POST /messages` los manda igual. Antes de mandar mensajes de marketing por la API, revisa `optedOut` en el contacto o guarda estas bajas en tu sistema.
</Warning>

## campaign.completed

Una campaña terminó de enviarse.

```json
{
  "id": "01a0d4a5-9f0a-7b1c-8d2e-3f4a5b6c7d8e",
  "event": "campaign.completed",
  "createdAt": "2026-10-15T13:18:40.221Z",
  "data": {
    "id": "01a0d480-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
    "name": "Día de la Madre — 20% off",
    "recipients": 1250,
    "sent": 1238,
    "delivered": 1190,
    "read": 702,
    "failed": 12,
    "completedAt": "2026-10-15T13:18:40.000Z"
  }
}
```

Los números son una foto del momento en que terminó el envío: las entregas y lecturas siguen subiendo durante las horas siguientes. Para los valores al día, consulta [`GET /campaigns`](/api-reference/campanas/listar).

## automation.completed

Una ejecución de una [automatización](/guias/automatizaciones/introduccion) terminó: llegó al final del flujo o pasó la conversación a una persona.

```json
{
  "id": "01a0d4a7-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
  "event": "automation.completed",
  "createdAt": "2026-09-25T14:03:11.508Z",
  "data": {
    "runId": "01a0d4c3-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "automationId": "01a0d4c2-7b1d-7e3f-9a40-5b6c7d8e9f01",
    "conversationId": "01a0d456-e5a1-7b2c-8d3e-4f5a6b7c8d9e",
    "contactId": "01a0d456-e419-7c69-a35f-38a00ae65b81",
    "status": "completed",
    "error": null,
    "vars": {
      "webhook": { "pedido": "1042" },
      "respuesta": "Excelente",
      "calificacion": "Excelente"
    }
  }
}
```

<ResponseField name="data.runId" type="string">Id de la ejecución. Es el `runId` que te devolvió el [webhook entrante](/guias/automatizaciones/integraciones#webhook-entrante) o [`POST /automations/{id}/runs`](/api-reference/automatizaciones/iniciar).</ResponseField>
<ResponseField name="data.automationId" type="string">La automatización.</ResponseField>
<ResponseField name="data.conversationId" type="string">La conversación en el chat de MINDO.</ResponseField>
<ResponseField name="data.contactId" type="string">El contacto.</ResponseField>
<ResponseField name="data.status" type="string">
  - `completed`: llegó al final del flujo.
  - `handed_off`: llegó a un paso **Pasar a una persona**.
</ResponseField>
<ResponseField name="data.error" type="string | null">Siempre `null` en este evento.</ResponseField>
<ResponseField name="data.vars" type="object">
  Las variables de la ejecución: las respuestas guardadas, lo que devolvieron las solicitudes HTTP, `webhook` con el `data` que mandaste al arrancarla y, en los comentarios de Instagram, `comentario`. `respuesta` es siempre la última respuesta a una pregunta.
</ResponseField>

<Tip>
  Es la forma más simple de traer a tu sistema lo que respondió el cliente: guarda cada respuesta en una variable con **Guardar en la variable** y léela de `data.vars` cuando llega este evento.
</Tip>

## automation.failed

Una ejecución no pudo seguir. Tiene los mismos datos que `automation.completed`, con `status: "failed"` y el motivo en `error`.

```json
{
  "id": "01a0d4a8-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
  "event": "automation.failed",
  "createdAt": "2026-09-25T14:05:42.117Z",
  "data": {
    "runId": "01a0d4c4-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
    "automationId": "01a0d4c2-7b1d-7e3f-9a40-5b6c7d8e9f01",
    "conversationId": "01a0d472-90ae-7187-9bb8-9f34c8d023c1",
    "contactId": "01a0d467-35b0-79f1-a6b6-1f834c8325eb",
    "status": "failed",
    "error": "La ventana de 24 h está cerrada: solo se puede escribir con una plantilla (WhatsApp) o cuando el cliente vuelva a escribir.",
    "vars": {
      "webhook": { "pedido": "1043" }
    }
  }
}
```

Los errores más comunes y cómo resolverlos están en [Probar y seguir las ejecuciones](/guias/automatizaciones/probar#errores-frecuentes).

<Note>
  Las ejecuciones **canceladas** (porque alguien del equipo respondió, alguien la detuvo o se pausó la automatización) y las **de prueba** no mandan ninguno de los dos eventos.
</Note>

## agent.goal_reached

Un [agente IA](/guias/agentes/introduccion) cumplió su objetivo con un cliente real. Las pruebas no lo mandan.

```json
{
  "id": "01a0e85c-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
  "event": "agent.goal_reached",
  "createdAt": "2026-09-28T14:11:42.118Z",
  "data": {
    "agentId": "01a0e84d-8992-7c7e-9f52-12630cbda55f",
    "agentName": "Sofi",
    "conversationId": "01a0e859-b693-7470-80cd-d3e07f010132",
    "contactId": "01a0e859-b5f1-7a2c-9e11-3c4d5e6f7a8b",
    "summary": "Valentina eligió el Kit de regalo Mañana Aurora; se le informó el envío a Córdoba y recibió la ficha para comprarlo.",
    "data": { "producto_interes": "Kit de regalo Mañana Aurora", "ciudad": "Córdoba" }
  }
}
```

<ResponseField name="data.agentId" type="string">El agente.</ResponseField>
<ResponseField name="data.agentName" type="string">Su nombre.</ResponseField>
<ResponseField name="data.conversationId" type="string">La conversación en el chat de MINDO.</ResponseField>
<ResponseField name="data.contactId" type="string">El contacto.</ResponseField>
<ResponseField name="data.summary" type="string | null">El resumen que dejó el agente.</ResponseField>
<ResponseField name="data.data" type="object">
  Los [datos del objetivo](/guias/agentes/crear#datos-que-tiene-que-conseguir) que consiguió, con sus claves. Solo trae los que el cliente llegó a dar.
</ResponseField>

## agent.handed_off

Un agente IA pasó una conversación a tu equipo: el cliente pidió una persona, tenía un reclamo o se dio uno de tus casos de derivación. Tiene los mismos datos que `agent.goal_reached`; en `summary` va el motivo y el resumen que quedó como nota interna en el chat.

```json
{
  "id": "01a0e85a-1f2e-7d3c-8b4a-5968778695a4",
  "event": "agent.handed_off",
  "createdAt": "2026-09-28T14:10:31.902Z",
  "data": {
    "agentId": "01a0e84d-8992-7c7e-9f52-12630cbda55f",
    "agentName": "Sofi",
    "conversationId": "01a0e859-d550-714d-8fac-e4d481777f98",
    "contactId": "01a0e859-d4a2-7b1c-8e0f-1a2b3c4d5e6f",
    "summary": "Consulta mayorista de 15 kg mensuales y factura A — Bar Molinos tiene un bar en Almagro y quiere cambiar de proveedor.",
    "data": {}
  }
}
```

## agent.conversation_reviewed

Se terminó el [análisis](/avanzados/analisis) de una conversación de un agente avanzado: el resultado de cada criterio y los datos extraídos. Llega una vez por cada tramo del agente en la conversación, y otra vez si alguien toca **Reanalizar**.

```json
{
  "id": "01a0e8b1-6c2d-7e3f-9a4b-5c6d7e8f9a0b",
  "event": "agent.conversation_reviewed",
  "createdAt": "2026-09-30T15:42:10.118Z",
  "data": {
    "agentId": "01a0e84d-8992-7c7e-9f52-12630cbda55f",
    "agentName": "Sofi",
    "conversationId": "01a0e859-d550-714d-8fac-e4d481777f98",
    "contactId": "01a0e859-d4a2-7b1c-8e0f-1a2b3c4d5e6f",
    "versionId": "01a0e87c-1b2a-7c3d-8e4f-5a6b7c8d9e0f",
    "endReason": "goal",
    "summary": "Valentina buscaba un regalo; eligió el Kit Mañana Aurora y recibió el link para comprarlo.",
    "criteria": [
      { "id": "4b1c2d3e-0f1a-4b2c-9d3e-4f5a6b7c8d9e", "name": "Resolvió la consulta", "result": "success", "score": null, "reason": "Recibió la ficha con el link de compra." },
      { "id": "5c2d3e4f-1a2b-4c3d-8e4f-5a6b7c8d9e0f", "name": "Tono", "result": null, "score": 5, "reason": "Respuestas cortas, cálidas y claras." }
    ],
    "data": { "producto_interes": "Kit de regalo Mañana Aurora", "presupuesto": 25000, "zona": "Interior" }
  }
}
```

<ResponseField name="data.versionId" type="string | null">La versión del agente que atendió.</ResponseField>
<ResponseField name="data.endReason" type="string">Por qué se dio por terminado el tramo: `goal` (cumplió el objetivo), `handoff` (derivó) o `idle` (una hora sin mensajes).</ResponseField>
<ResponseField name="data.summary" type="string | null">Una o dos frases con lo que pasó.</ResponseField>
<ResponseField name="data.criteria" type="object[]">
  Un elemento por criterio. `result` es `success`, `failure` o `unknown` en los criterios de bien o mal (y `null` en los de puntaje); `score` es de 1 a 5 en los de puntaje (o `null`).
</ResponseField>
<ResponseField name="data.data" type="object">Los datos extraídos, con sus claves. Los que no aparecieron vienen en `null`.</ResponseField>

## case.opened, case.classified, case.closed, case.outcome y case.stage_changed

Los [casos](/guias/chat/casos): cada consulta de un cliente, con su motivo, su puntaje comercial y su resultado. Los cinco eventos traen el caso entero en `data`:

- `case.opened`: un cliente escribió y se abrió un caso nuevo.
- `case.classified`: Jev le puso motivo o calculó el puntaje.
- `case.closed`: se cerró (pasaron sus horas sin mensajes, alguien lo cerró, se cerró la conversación, el agente IA cumplió su objetivo o pasó a una etapa final de su pipeline).
- `case.outcome`: se marcó la venta o que se perdió, desde el chat o [por la API](/api-reference/casos/resultado).
- `case.stage_changed`: el caso entró a un [pipeline](/guias/pipelines/introduccion) o cambió de etapa: lo movió una persona, una regla de la etapa, la falta de respuesta del cliente, una automatización o [la API](/api-reference/pipelines/mover-etapa).

```json
{
  "id": "01a1180a-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
  "event": "case.outcome",
  "createdAt": "2026-10-08T13:12:44.512Z",
  "data": {
    "id": "01a11809-7469-741c-9183-2d5abfb2f040",
    "conversationId": "01a11809-7124-718f-9140-76e63b646ed7",
    "contactId": "01a11809-7055-71ce-a38b-959107993149",
    "status": "open",
    "openedAt": "2026-10-07T20:23:51.000Z",
    "closedAt": null,
    "closeReason": null,
    "reason": { "id": "01a117f0-0b1c-7d2e-8f3a-4b5c6d7e8f90", "name": "Comercial" },
    "subreason": { "id": "01a117f0-0b1d-7e2f-9a3b-5c6d7e8f9a01", "name": "Consulta de precio" },
    "reasonSource": "jev",
    "reasonConfidence": 0.88,
    "handledBy": "ai",
    "adId": "120210000000000001",
    "score": 82,
    "outcome": { "result": "won", "amount": 15000, "currency": "ARS", "note": null, "externalId": "pedido-1042", "source": "api" },
    "pipeline": {
      "id": "01a117f1-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
      "name": "Ventas",
      "stage": { "id": "01a117f2-1c2d-7e3f-8a4b-5c6d7e8f9a15", "name": "Ganado", "kind": "won" },
      "enteredAt": "2026-10-08T13:12:44.000Z",
      "amount": 15000,
      "fields": { "producto": "Sillón de 3 cuerpos" }
    }
  }
}
```

<ResponseField name="data.reason / data.subreason" type="object | null">El motivo y el submotivo, con su id y su nombre.</ResponseField>
<ResponseField name="data.reasonSource" type="string | null">Quién puso el motivo: `jev` o `person` (alguien del equipo).</ResponseField>
<ResponseField name="data.handledBy" type="string">Quién respondió: `none`, `automation`, `ai`, `human` o `mixed` (agente IA y equipo).</ResponseField>
<ResponseField name="data.adId" type="string | null">El anuncio de Meta que trajo al cliente, si vino de uno en los 7 días anteriores.</ResponseField>
<ResponseField name="data.score" type="number | null">El puntaje comercial de 0 a 100, si el motivo lo lleva.</ResponseField>
<ResponseField name="data.closeReason" type="string | null">`inactivity`, `manual`, `agent_goal`, `conversation_closed`, `stage` (pasó a una etapa final de su pipeline) o `stale` (pasó solo a perdido por días sin respuesta del cliente).</ResponseField>
<ResponseField name="data.outcome" type="object | null">El resultado: `result` (`won` o `lost`), el monto y la moneda, la nota, tu `externalId` y si lo marcó una persona (`person`) o llegó por la API (`api`).</ResponseField>
<ResponseField name="data.pipeline" type="object | null">La etapa del caso, si su motivo está en un pipeline: el pipeline (`id`, `name`), la etapa (`stage` con `id`, `name` y `kind`: `open`, `won` o `lost`), cuándo entró (`enteredAt`), el monto estimado (`amount`, solo ventas) y los valores de sus campos (`fields`). `null` si no está en ninguno.</ResponseField>

## Evento de prueba

El botón **Probar** de **Ajustes › API y webhooks** manda este `message.received`, con `"test": true` y datos de ejemplo:

```json
{
  "id": "01a0d46c-5d2a-7a1b-9c3d-4e5f6a7b8c9d",
  "event": "message.received",
  "test": true,
  "createdAt": "2026-09-24T17:09:03.458Z",
  "data": {
    "text": "Mensaje de prueba de MINDO",
    "contact": { "phone": "5491100000000", "name": "Prueba" }
  }
}
```

Verifica la firma igual que siempre, pero no lo proceses como un mensaje real.
