> ## Documentation Index
> Fetch the complete documentation index at: https://developers.nuvia.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Reciba eventos de Nuvia en su servidor: formato del payload, eventos disponibles y garantías de entrega

Un webhook hace el camino inverso de la API: en vez de que usted consulte a Nuvia, Nuvia envía un
`POST` a su servidor cada vez que algo sucede en su operación: llega un mensaje, se crea un
contacto, el agente completa un campo.

<Note>
  Cada empresa tiene **como máximo un** webhook. No es una colección: usted configura una URL y
  elige qué eventos recibe. Intentar crear un segundo devuelve `409`.
</Note>

## Configurar

`POST /v1/webhooks` con la URL y la lista de eventos. Scope necesario: `webhooks:create`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.nuvia.ai/v1/webhooks \
    -H "Authorization: Bearer $NUVIA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://su-servidor.com/nuvia/webhook",
      "events": ["MESSAGE_RECEIVED", "CONTACT_CREATED"],
      "custom_headers": { "X-Mi-Token": "un-secreto-suyo" },
      "status": "ACTIVE"
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch("https://api.nuvia.ai/v1/webhooks", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.NUVIA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: "https://su-servidor.com/nuvia/webhook",
      events: ["MESSAGE_RECEIVED", "CONTACT_CREATED"],
      custom_headers: { "X-Mi-Token": "un-secreto-suyo" },
      status: "ACTIVE",
    }),
  });
  ```

  ```python Python theme={null}
  import os, requests
  requests.post(
      "https://api.nuvia.ai/v1/webhooks",
      headers={"Authorization": f"Bearer {os.environ['NUVIA_API_KEY']}"},
      json={
          "url": "https://su-servidor.com/nuvia/webhook",
          "events": ["MESSAGE_RECEIVED", "CONTACT_CREATED"],
          "custom_headers": {"X-Mi-Token": "un-secreto-suyo"},
          "status": "ACTIVE",
      },
  )
  ```
</CodeGroup>

| Campo            | Tipo                   | Obligatorio | Descripción                                                            |
| ---------------- | ---------------------- | ----------- | ---------------------------------------------------------------------- |
| `url`            | string                 | Sí          | URL que recibirá los `POST`.                                           |
| `events`         | string\[]              | Sí          | Al menos un evento de la tabla de abajo.                               |
| `custom_headers` | objeto                 | No          | Headers extra enviados en cada entrega. Úselos para validar el origen. |
| `status`         | `ACTIVE` \| `INACTIVE` | No          | Por defecto `ACTIVE`. En `INACTIVE` no se entrega nada.                |

## Qué envía Nuvia

Cada entrega es un `POST` con estos headers:

| Header            | Valor                                                   |
| ----------------- | ------------------------------------------------------- |
| `Content-Type`    | `application/json`                                      |
| `X-Webhook-Event` | El nombre del evento, igual al campo `event` del cuerpo |
| *(sus headers)*   | Todo lo que configuró en `custom_headers`               |

Y el cuerpo siempre tiene la misma estructura de tres campos:

```json theme={null}
{
  "event": "MESSAGE_RECEIVED",
  "timestamp": "2026-08-13T14:03:00.000Z",
  "data": {}
}
```

* **`event`**: el evento que disparó la entrega.
* **`timestamp`**: ISO 8601 UTC, generado en el momento en que la entrega fue encolada.
* **`data`**: el recurso relacionado con el evento (ver la tabla de abajo).

<Warning>
  El cuerpo **no** identifica a la empresa: no existe `companyId` en el envelope. Si su servidor
  atiende a varias empresas de Nuvia, use una URL por empresa o un valor distinto en
  `custom_headers` para saber de quién es el evento.
</Warning>

## Eventos disponibles

| Evento                 | Cuándo se dispara                           | Qué viene en `data`                      |
| ---------------------- | ------------------------------------------- | ---------------------------------------- |
| `MESSAGE_RECEIVED`     | Mensaje recibido de un contacto             | El mensaje                               |
| `MESSAGE_SENT`         | Mensaje enviado por su operación            | El mensaje                               |
| `MESSAGE_DELIVERED`    | WhatsApp confirmó la entrega                | El mensaje, con el estado ya actualizado |
| `MESSAGE_READ`         | El contacto leyó el mensaje                 | El mensaje, con el estado ya actualizado |
| `CONVERSATION_CREATED` | Nueva conversación abierta                  | La conversación                          |
| `CONVERSATION_UPDATED` | Conversación modificada                     | La conversación                          |
| `CONTACT_CREATED`      | Nuevo contacto creado                       | El contacto                              |
| `CONTACT_UPDATED`      | Contacto modificado                         | El contacto                              |
| `FOLLOWUP_SENT`        | Follow-up disparado por el agente           | El follow-up                             |
| `FIELD_UPDATED`        | Campo personalizado completado o modificado | Estructura propia (ver abajo)            |

En los eventos de mensaje, conversación y contacto, `data` trae el recurso en el mismo formato que
la [Referencia de la API](/es/api-reference) devuelve en los `GET` correspondientes, por ejemplo
`GET /v1/messages/conversation/{id}` para mensajes.

<Note>
  Trate `data` de forma tolerante: lea los campos que use e ignore el resto. Pueden aparecer
  campos nuevos a medida que la plataforma evoluciona, y eso no se considera una ruptura de
  contrato.
</Note>

### `FIELD_UPDATED` es diferente

Este evento no devuelve una entidad, sino la descripción del cambio:

```json theme={null}
{
  "event": "FIELD_UPDATED",
  "timestamp": "2026-08-13T14:03:00.000Z",
  "data": {
    "field": {
      "_id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "slug": "orcamento",
      "title": "Orçamento",
      "context": "contact"
    },
    "value": "50000",
    "contactId": "65f1a2b3c4d5e6f7a8b9c0d2",
    "conversationId": "65f1a2b3c4d5e6f7a8b9c0d3",
    "companyId": "65f1a2b3c4d5e6f7a8b9c0d4"
  }
}
```

`field.context` indica a qué pertenece el campo: `contact`, `conversation` o `business`. `value`
tiene el tipo del campo: string, número, booleano o lista. `conversationId` solo viene cuando el
cambio ocurrió dentro de una conversación.

## Validar que la llamada vino de Nuvia

<Warning>
  **No hay firma HMAC.** Nuvia no firma el cuerpo de la solicitud, así que no existe forma de
  verificar criptográficamente el origen.
</Warning>

El mecanismo disponible es `custom_headers`: registre un valor secreto y verifíquelo en cada
solicitud.

```javascript Node.js theme={null}
app.post("/nuvia/webhook", (req, res) => {
  if (req.get("X-Mi-Token") !== process.env.NUVIA_WEBHOOK_TOKEN) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // responda primero
  procesarEnSegundoPlano(req.body); // procese después
});
```

Use HTTPS y un valor largo y aleatorio. Como el secreto viaja en el header en cada entrega,
trátelo como una credencial: rótelo con `PUT /v1/webhooks/{id}` si sospecha de una filtración.

## Garantías de entrega

Estas son las características reales de la entrega. Léalas antes de construir algo que dependa de
un webhook:

<AccordionGroup>
  <Accordion title="Un solo intento por evento, sin reintento" icon="rotate-left">
    Si su servidor está fuera de línea, devuelve `5xx` o agota el tiempo límite, **el evento se
    pierde**. La falla queda registrada del lado de Nuvia, pero nada se reenvía. Para datos
    críticos, reconcilie periódicamente con la API (por ejemplo, listando los mensajes de la
    conversación) en vez de confiar solo en el webhook.
  </Accordion>

  <Accordion title="Tiempo límite de 10 segundos" icon="clock">
    Nuvia espera como máximo 10 segundos su respuesta. Responda `2xx` de inmediato y haga el
    procesamiento en segundo plano. Si procesa antes de responder, entregas legítimas se
    convierten en timeout.
  </Accordion>

  <Accordion title="El orden no está garantizado" icon="arrow-down-up-across-line">
    Los eventos de estado vienen de WhatsApp, que puede entregarlos fuera de orden (un
    `MESSAGE_DELIVERED` llegando después del `MESSAGE_READ` del mismo mensaje). Nuvia no los
    reordena. Trate cada evento por el estado que trae, sin asumir una secuencia.
  </Accordion>

  <Accordion title="Los eventos de estado solo se disparan en la transición real" icon="filter">
    `MESSAGE_DELIVERED` y `MESSAGE_READ` solo se envían cuando el estado realmente cambia.
    WhatsApp reenvía el mismo estado varias veces, y esas repeticiones se descartan: usted no
    recibe duplicados por eso. Aun así, escriba un handler idempotente: use el identificador del
    mensaje como clave.
  </Accordion>

  <Accordion title="Nada se entrega con el webhook inactivo" icon="power-off">
    Con `status: "INACTIVE"`, o para eventos que no incluyó en `events`, Nuvia no encola ninguna
    entrega, y no hay historial para recuperar después. Reactivarlo no recupera el período
    detenido.
  </Accordion>
</AccordionGroup>

## Gestionar la configuración

| Acción     | Llamada                    | Scope             |
| ---------- | -------------------------- | ----------------- |
| Consultar  | `GET /v1/webhooks`         | `webhooks:read`   |
| Actualizar | `PUT /v1/webhooks/{id}`    | `webhooks:update` |
| Eliminar   | `DELETE /v1/webhooks/{id}` | `webhooks:delete` |

El `GET` devuelve **un objeto** con la configuración de la empresa, o `null` si no hay ninguna.
No es una lista. El `id` usado en el `PUT` y en el `DELETE` viene de ese objeto. Después de
eliminarlo, la empresa vuelve a poder crear un webhook nuevo.

Para pausar temporalmente sin perder la configuración, prefiera `PUT` con `status: "INACTIVE"` en
vez de eliminarlo.

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Ejemplos de integración" icon="plug" href="/es/guias/exemplos-integracao">
    El webhook dentro de un caso de uso completo, con respuesta automática.
  </Card>

  <Card title="Referencia de la API" icon="code" href="/es/api-reference/webhooks">
    Parámetros y respuestas de los endpoints de webhook.
  </Card>
</CardGroup>
