# Avisos (webhooks)

DoneTalk avisa a su sistema lo que pasa, con un POST firmado a una dirección suya: una campaña empezó o terminó, un mensaje fue entregado o leído, un contacto respondió, una conversación concluyó con su desenlace.

## Registrar una dirección

En **Configuración › Integraciones › Webhooks de salida**, o por la API con el alcance `webhooks:manage`:

```bash
curl -X POST https://api.donetalk.com/v1/c/{empresa}/webhooks \
  -H "Authorization: Bearer $DONETALK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://su-sistema.com/donetalk/avisos", "events": ["campaign.completed", "campaign.recipient.replied", "conversation.closed"]}'
```

La respuesta trae el `secret` una sola vez: guárdelo para verificar las firmas.

## Qué llega

```http
POST /donetalk/avisos HTTP/1.1
Content-Type: application/json
User-Agent: DoneTalk-Webhooks/1.0
DoneTalk-Signature: t=1792756800,v1=5f0c…
X-DoneTalk-Event: conversation.closed
X-DoneTalk-Delivery: 01a3e4f5-a6b7-7c8d-9e0f-1a2b3c4d5e6f
```

```json
{
  "id": "01a3e4f5-…",
  "type": "conversation.closed",
  "occurredAt": "2026-10-24T12:20:41.000Z",
  "customerId": "01a0c0de-…",
  "data": { "...": "el contenido del evento" }
}
```

Los avisos de una clave de prueba traen `"test": true`.

## Verificar la firma

`DoneTalk-Signature` trae `t` (la hora del envío, en segundos) y `v1`, el HMAC SHA-256 de `"<t>.<cuerpo exacto>"` con su `secret`. Rechace avisos con un `t` de hace más de 5 minutos: así nadie reenvía un aviso antiguo.

```js
import crypto from "node:crypto";

function avisoValido(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const esperado = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(v1));
}
```

```python
import hmac, hashlib, time

def aviso_valido(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    esperado = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, parts["v1"])
```

El encabezado antiguo `x-donetalk-signature` (`sha256=` + HMAC del cuerpo, sin hora) sigue junto hasta el 21/01/2027.

## Responder y reintentos

Responda `2xx` en hasta 10 segundos; haga el trabajo pesado después. Sin `2xx`, DoneTalk reintenta a 1 min, 5 min, 30 min, 2 h y 12 h. Tras 20 fallas seguidas la dirección aparece como "Con fallos" (sigue recibiendo). Un mismo aviso puede llegar más de una vez: use su `id` para procesarlo una vez. El orden de llegada no está garantizado: use `occurredAt`.

## Eventos

| Evento | Cuándo llega |
|---|---|
| `conversation.opened` | Una conversación empezó (el contacto escribió o hizo clic en un anuncio). |
| `conversation.closed` | Una conversación concluyó, con el desenlace, quién la concluyó (persona, asistente de IA o regla automática), la nota de cierre, el contacto, el equipo, las etiquetas, cuánto duró y la campaña de origen. |
| `conversation.assigned` | Una conversación fue asignada a una persona. |
| `conversation.transferred` | Una conversación pasó a otro equipo o persona. |
| `conversation.reopened` | Una conversación concluida fue reabierta. |
| `contact.updated` | Los datos de un contacto cambiaron. |
| `contact.consent_changed` | El contacto aceptó o rechazó recibir mensajes de una finalidad (marketing, por ejemplo). |
| `message.received` | El contacto envió un mensaje. |
| `message.sent` | Un mensaje nuestro salió hacia WhatsApp. |
| `message.delivered` | Un mensaje llegó al teléfono del contacto. |
| `message.read` | El contacto leyó un mensaje. |
| `message.failed` | Un mensaje no pudo entregarse; `error.code` es el código de Meta. |
| `flow.event` | Un flujo llegó a un bloque “Registrar evento”, con el nombre y los datos que define el bloque. |
| `appointment.booked` | Una cita fue agendada. |
| `appointment.rescheduled` | Una cita cambió de horario. |
| `appointment.cancelled` | Una cita fue cancelada. |
| `appointment.confirmed` | El contacto confirmó la cita. |
| `appointment.attended` | El contacto asistió. |
| `appointment.no_show` | El contacto no asistió. |
| `campaign.started` | La campaña empezó a enviar; su lista quedó cerrada. |
| `campaign.paused` | La campaña se pausó: por una persona, la API o una regla (plantilla pausada, calidad baja, muchas bajas, presupuesto). |
| `campaign.resumed` | La campaña volvió a enviar. |
| `campaign.completed` | La campaña terminó de enviar, con sus totales. |
| `campaign.cancelled` | La campaña fue cancelada. |
| `campaign.recipient.sent` | El mensaje de la campaña salió hacia un contacto. |
| `campaign.recipient.delivered` | El mensaje de la campaña llegó al contacto. |
| `campaign.recipient.read` | El contacto leyó el mensaje de la campaña. |
| `campaign.recipient.replied` | El contacto respondió a la campaña (la primera respuesta), por texto o tocando un botón. |
| `campaign.recipient.failed` | El mensaje de la campaña falló para un contacto; `errorCode` es el código de Meta. |
| `campaign.recipient.opted_out` | El contacto pidió no recibir más campañas (botón “Parar promociones” o rechazo en Meta). |

## Ejemplo de cada evento

### conversation.opened

Una conversación empezó (el contacto escribió o hizo clic en un anuncio).

```json
{
  "id": "01a3e4f5-…",
  "type": "conversation.opened",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "handler": "flow",
    "source": "inbound"
  }
}
```

### conversation.closed

Una conversación concluyó, con el desenlace, quién la concluyó (persona, asistente de IA o regla automática), la nota de cierre, el contacto, el equipo, las etiquetas, cuánto duró y la campaña de origen.

```json
{
  "id": "01a3e4f5-…",
  "type": "conversation.closed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "teamId": "01a0cb22-89c0-7e02-b96b-3f4731dde701",
    "assigneeId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "outcome": "converted",
    "isSuccess": true,
    "closedBy": {
      "kind": "member",
      "id": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
      "name": "Tayomara Batista"
    },
    "actorKind": "member",
    "openedAt": "2026-10-24T12:00:04.000Z",
    "firstResponseAt": "2026-10-24T12:05:10.000Z",
    "messagesIn": 6,
    "messagesOut": 7,
    "outcomeLabel": {
      "pt-BR": "Venda",
      "en": "Sale",
      "es": "Venta"
    },
    "outcomeCategory": "success",
    "note": "Fechou o plano anual com o cupom ANA10.",
    "contact": {
      "name": "Ana Souza",
      "phone": "5548999990001"
    },
    "team": {
      "id": "01a0cb22-89c0-7e02-b96b-3f4731dde701",
      "name": "Vendas"
    },
    "tags": [
      {
        "id": "01a12150-3610-7c00-b680-188c2b4c7313",
        "name": {
          "pt-BR": "VIP",
          "en": "VIP",
          "es": "VIP"
        }
      }
    ],
    "durationSeconds": 1237,
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b"
  }
}
```

### conversation.assigned

Una conversación fue asignada a una persona.

```json
{
  "id": "01a3e4f5-…",
  "type": "conversation.assigned",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "assigneeId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "teamId": "01a0cb22-89c0-7e02-b96b-3f4731dde701",
    "reason": "distribution",
    "previousAssigneeId": null,
    "actorKind": "system"
  }
}
```

### conversation.transferred

Una conversación pasó a otro equipo o persona.

```json
{
  "id": "01a3e4f5-…",
  "type": "conversation.transferred",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "teamId": "01a0cb22-89c0-7e02-b96b-3f4731dde701",
    "assigneeId": null,
    "previousTeamId": null,
    "previousAssigneeId": null,
    "previousHandler": "flow",
    "actorKind": "flow",
    "reason": "Gestor de conta: Tayomara Batista"
  }
}
```

### conversation.reopened

Una conversación concluida fue reabierta.

```json
{
  "id": "01a3e4f5-…",
  "type": "conversation.reopened",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677"
  }
}
```

### contact.updated

Los datos de un contacto cambiaron.

```json
{
  "id": "01a3e4f5-…",
  "type": "contact.updated",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "changes": [
      "fields",
      "displayName"
    ]
  }
}
```

### contact.consent_changed

El contacto aceptó o rechazó recibir mensajes de una finalidad (marketing, por ejemplo).

```json
{
  "id": "01a3e4f5-…",
  "type": "contact.consent_changed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "channel": "whatsapp",
    "purpose": "marketing",
    "status": "opted_out",
    "source": "keyword",
    "occurredAt": "2026-10-24T12:06:00.000Z"
  }
}
```

### message.received

El contacto envió un mensaje.

```json
{
  "id": "01a3e4f5-…",
  "type": "message.received",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "type": "text"
  }
}
```

### message.sent

Un mensaje nuestro salió hacia WhatsApp.

```json
{
  "id": "01a3e4f5-…",
  "type": "message.sent",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "providerMessageId": "wamid.HBgMNTU0ODk5OTk5MDAwMRUCABEYEjE="
  }
}
```

### message.delivered

Un mensaje llegó al teléfono del contacto.

```json
{
  "id": "01a3e4f5-…",
  "type": "message.delivered",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "delivered",
    "occurredAt": "2026-10-24T12:00:06.000Z"
  }
}
```

### message.read

El contacto leyó un mensaje.

```json
{
  "id": "01a3e4f5-…",
  "type": "message.read",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "read",
    "occurredAt": "2026-10-24T12:03:51.000Z"
  }
}
```

### message.failed

Un mensaje no pudo entregarse; `error.code` es el código de Meta.

```json
{
  "id": "01a3e4f5-…",
  "type": "message.failed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "failed",
    "occurredAt": "2026-10-24T12:00:06.000Z",
    "error": {
      "code": "131026",
      "detail": "Message undeliverable"
    }
  }
}
```

### flow.event

Un flujo llegó a un bloque “Registrar evento”, con el nombre y los datos que define el bloque.

```json
{
  "id": "01a3e4f5-…",
  "type": "flow.event",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "name": "lead.qualified",
    "data": {
      "plano": "anual",
      "origem": "site"
    }
  }
}
```

### appointment.booked

Una cita fue agendada.

```json
{
  "id": "01a3e4f5-…",
  "type": "appointment.booked",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "id": "01a2f00d-1234-7abc-8def-0123456789ab",
    "agendaId": "01a1228c-c584-73f7-b7e7-bfaa0d02fed0",
    "agendaName": "Visitas aos imóveis",
    "typeId": "01a1228c-c58f-796a-b3aa-54a953711c71",
    "typeName": "Visita ao imóvel",
    "typeColor": "#1daa61",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "contactName": "Ana Souza",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "memberId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "memberName": "Tayomara Batista",
    "holderIds": [
      "01a0cac1-4a6f-7646-b5f8-c1d19090016a"
    ],
    "status": "booked",
    "startsAt": "2026-10-27T13:00:00.000Z",
    "endsAt": "2026-10-27T14:00:00.000Z",
    "timezone": "America/Sao_Paulo",
    "location": "Rua Bahia, 120 – Higienópolis, São Paulo",
    "locationKind": "address",
    "onlineUrl": null,
    "answers": {
      "imovel": "AP-2231"
    },
    "notes": null,
    "source": "api",
    "createdByKind": "api_key",
    "createdById": null,
    "confirmedAt": null,
    "cancelledAt": null,
    "cancelledByKind": null,
    "cancelReason": null,
    "rescheduleCount": 0,
    "previousStartsAt": null,
    "publicToken": "r3WqZ8",
    "createdAt": "2026-10-23T13:02:11.000Z"
  }
}
```

### appointment.rescheduled

Una cita cambió de horario.

```json
{
  "id": "01a3e4f5-…",
  "type": "appointment.rescheduled",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "id": "01a2f00d-1234-7abc-8def-0123456789ab",
    "agendaId": "01a1228c-c584-73f7-b7e7-bfaa0d02fed0",
    "agendaName": "Visitas aos imóveis",
    "typeId": "01a1228c-c58f-796a-b3aa-54a953711c71",
    "typeName": "Visita ao imóvel",
    "typeColor": "#1daa61",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "contactName": "Ana Souza",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "memberId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "memberName": "Tayomara Batista",
    "holderIds": [
      "01a0cac1-4a6f-7646-b5f8-c1d19090016a"
    ],
    "status": "booked",
    "startsAt": "2026-10-27T13:00:00.000Z",
    "endsAt": "2026-10-27T14:00:00.000Z",
    "timezone": "America/Sao_Paulo",
    "location": "Rua Bahia, 120 – Higienópolis, São Paulo",
    "locationKind": "address",
    "onlineUrl": null,
    "answers": {
      "imovel": "AP-2231"
    },
    "notes": null,
    "source": "api",
    "createdByKind": "api_key",
    "createdById": null,
    "confirmedAt": null,
    "cancelledAt": null,
    "cancelledByKind": null,
    "cancelReason": null,
    "rescheduleCount": 1,
    "previousStartsAt": "2026-10-27T12:00:00.000Z",
    "publicToken": "r3WqZ8",
    "createdAt": "2026-10-23T13:02:11.000Z"
  }
}
```

### appointment.cancelled

Una cita fue cancelada.

```json
{
  "id": "01a3e4f5-…",
  "type": "appointment.cancelled",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "id": "01a2f00d-1234-7abc-8def-0123456789ab",
    "agendaId": "01a1228c-c584-73f7-b7e7-bfaa0d02fed0",
    "agendaName": "Visitas aos imóveis",
    "typeId": "01a1228c-c58f-796a-b3aa-54a953711c71",
    "typeName": "Visita ao imóvel",
    "typeColor": "#1daa61",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "contactName": "Ana Souza",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "memberId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "memberName": "Tayomara Batista",
    "holderIds": [
      "01a0cac1-4a6f-7646-b5f8-c1d19090016a"
    ],
    "status": "cancelled",
    "startsAt": "2026-10-27T13:00:00.000Z",
    "endsAt": "2026-10-27T14:00:00.000Z",
    "timezone": "America/Sao_Paulo",
    "location": "Rua Bahia, 120 – Higienópolis, São Paulo",
    "locationKind": "address",
    "onlineUrl": null,
    "answers": {
      "imovel": "AP-2231"
    },
    "notes": null,
    "source": "api",
    "createdByKind": "api_key",
    "createdById": null,
    "confirmedAt": null,
    "cancelledAt": "2026-10-26T18:00:00.000Z",
    "cancelledByKind": "contact",
    "cancelReason": null,
    "rescheduleCount": 0,
    "previousStartsAt": null,
    "publicToken": "r3WqZ8",
    "createdAt": "2026-10-23T13:02:11.000Z"
  }
}
```

### appointment.confirmed

El contacto confirmó la cita.

```json
{
  "id": "01a3e4f5-…",
  "type": "appointment.confirmed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "id": "01a2f00d-1234-7abc-8def-0123456789ab",
    "agendaId": "01a1228c-c584-73f7-b7e7-bfaa0d02fed0",
    "agendaName": "Visitas aos imóveis",
    "typeId": "01a1228c-c58f-796a-b3aa-54a953711c71",
    "typeName": "Visita ao imóvel",
    "typeColor": "#1daa61",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "contactName": "Ana Souza",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "memberId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "memberName": "Tayomara Batista",
    "holderIds": [
      "01a0cac1-4a6f-7646-b5f8-c1d19090016a"
    ],
    "status": "confirmed",
    "startsAt": "2026-10-27T13:00:00.000Z",
    "endsAt": "2026-10-27T14:00:00.000Z",
    "timezone": "America/Sao_Paulo",
    "location": "Rua Bahia, 120 – Higienópolis, São Paulo",
    "locationKind": "address",
    "onlineUrl": null,
    "answers": {
      "imovel": "AP-2231"
    },
    "notes": null,
    "source": "api",
    "createdByKind": "api_key",
    "createdById": null,
    "confirmedAt": "2026-10-26T14:00:00.000Z",
    "cancelledAt": null,
    "cancelledByKind": null,
    "cancelReason": null,
    "rescheduleCount": 0,
    "previousStartsAt": null,
    "publicToken": "r3WqZ8",
    "createdAt": "2026-10-23T13:02:11.000Z"
  }
}
```

### appointment.attended

El contacto asistió.

```json
{
  "id": "01a3e4f5-…",
  "type": "appointment.attended",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "id": "01a2f00d-1234-7abc-8def-0123456789ab",
    "agendaId": "01a1228c-c584-73f7-b7e7-bfaa0d02fed0",
    "agendaName": "Visitas aos imóveis",
    "typeId": "01a1228c-c58f-796a-b3aa-54a953711c71",
    "typeName": "Visita ao imóvel",
    "typeColor": "#1daa61",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "contactName": "Ana Souza",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "memberId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "memberName": "Tayomara Batista",
    "holderIds": [
      "01a0cac1-4a6f-7646-b5f8-c1d19090016a"
    ],
    "status": "attended",
    "startsAt": "2026-10-27T13:00:00.000Z",
    "endsAt": "2026-10-27T14:00:00.000Z",
    "timezone": "America/Sao_Paulo",
    "location": "Rua Bahia, 120 – Higienópolis, São Paulo",
    "locationKind": "address",
    "onlineUrl": null,
    "answers": {
      "imovel": "AP-2231"
    },
    "notes": null,
    "source": "api",
    "createdByKind": "api_key",
    "createdById": null,
    "confirmedAt": null,
    "cancelledAt": null,
    "cancelledByKind": null,
    "cancelReason": null,
    "rescheduleCount": 0,
    "previousStartsAt": null,
    "publicToken": "r3WqZ8",
    "createdAt": "2026-10-23T13:02:11.000Z"
  }
}
```

### appointment.no_show

El contacto no asistió.

```json
{
  "id": "01a3e4f5-…",
  "type": "appointment.no_show",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "id": "01a2f00d-1234-7abc-8def-0123456789ab",
    "agendaId": "01a1228c-c584-73f7-b7e7-bfaa0d02fed0",
    "agendaName": "Visitas aos imóveis",
    "typeId": "01a1228c-c58f-796a-b3aa-54a953711c71",
    "typeName": "Visita ao imóvel",
    "typeColor": "#1daa61",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "contactName": "Ana Souza",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "channelId": "01a0cccc-0000-7000-8000-000000000002",
    "memberId": "01a0cac1-4a6f-7646-b5f8-c1d19090016a",
    "memberName": "Tayomara Batista",
    "holderIds": [
      "01a0cac1-4a6f-7646-b5f8-c1d19090016a"
    ],
    "status": "no_show",
    "startsAt": "2026-10-27T13:00:00.000Z",
    "endsAt": "2026-10-27T14:00:00.000Z",
    "timezone": "America/Sao_Paulo",
    "location": "Rua Bahia, 120 – Higienópolis, São Paulo",
    "locationKind": "address",
    "onlineUrl": null,
    "answers": {
      "imovel": "AP-2231"
    },
    "notes": null,
    "source": "api",
    "createdByKind": "api_key",
    "createdById": null,
    "confirmedAt": null,
    "cancelledAt": null,
    "cancelledByKind": null,
    "cancelReason": null,
    "rescheduleCount": 0,
    "previousStartsAt": null,
    "publicToken": "r3WqZ8",
    "createdAt": "2026-10-23T13:02:11.000Z"
  }
}
```

### campaign.started

La campaña empezó a enviar; su lista quedó cerrada.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.started",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "running",
    "audience": 1240,
    "skipped": 24,
    "startedAt": "2026-10-24T12:00:00.000Z"
  }
}
```

### campaign.paused

La campaña se pausó: por una persona, la API o una regla (plantilla pausada, calidad baja, muchas bajas, presupuesto).

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.paused",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "paused",
    "reason": "optout_rate",
    "by": "system"
  }
}
```

### campaign.resumed

La campaña volvió a enviar.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.resumed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "running",
    "by": "api_key"
  }
}
```

### campaign.completed

La campaña terminó de enviar, con sus totales.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.completed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "completed",
    "totals": {
      "audience": 1216,
      "sent": 1216,
      "delivered": 1198,
      "read": 874,
      "replied": 141,
      "failed": 18,
      "skipped": 24,
      "optedOut": 6
    }
  }
}
```

### campaign.cancelled

La campaña fue cancelada.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.cancelled",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "status": "cancelled",
    "by": "member"
  }
}
```

### campaign.recipient.sent

El mensaje de la campaña salió hacia un contacto.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.recipient.sent",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "phone": "5548999990001",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "at": "2026-10-24T12:00:04.000Z"
  }
}
```

### campaign.recipient.delivered

El mensaje de la campaña llegó al contacto.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.recipient.delivered",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "phone": "5548999990001",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "at": "2026-10-24T12:00:06.000Z"
  }
}
```

### campaign.recipient.read

El contacto leyó el mensaje de la campaña.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.recipient.read",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "phone": "5548999990001",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "at": "2026-10-24T12:03:51.000Z"
  }
}
```

### campaign.recipient.replied

El contacto respondió a la campaña (la primera respuesta), por texto o tocando un botón.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.recipient.replied",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "buttonIndex": 0,
    "text": "Quero o cupom"
  }
}
```

### campaign.recipient.failed

El mensaje de la campaña falló para un contacto; `errorCode` es el código de Meta.

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.recipient.failed",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "phone": "5548999990001",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "messageId": "01a3c0de-8b22-7c33-9d44-0e55f6677889",
    "errorCode": "131026"
  }
}
```

### campaign.recipient.opted_out

El contacto pidió no recibir más campañas (botón “Parar promociones” o rechazo en Meta).

```json
{
  "id": "01a3e4f5-…",
  "type": "campaign.recipient.opted_out",
  "occurredAt": "2026-10-24T12:04:02.000Z",
  "customerId": "01a0c0de-…",
  "data": {
    "campaignId": "01a2b3c4-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "contactId": "01a0d05a-3ee3-75bd-9db5-dc9856f079c8",
    "conversationId": "01a3c0de-7a11-7b22-8c33-9d44e55f6677",
    "via": "button"
  }
}
```
