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

# Avisos (webhooks)

> Recibe en tu sistema lo que pasa en la cuenta, al instante y firmado.

Un aviso es una dirección de tu sistema a la que Impulso le envía un `POST` cada vez que pasa algo.
Créalo en el panel (**Integraciones → Para desarrolladores → Nuevo aviso**) o por la API (`POST /webhooks`).
La respuesta trae el `secret` **una sola vez**: guárdalo para verificar la firma.

## Eventos

| event | Cuándo |
| - | - |
| `message.received` | El cliente escribió (texto, foto, audio, documento, ubicación…) |
| `message.sent` | Salió un mensaje al cliente (del agente, del equipo, programado o desde el celular) |
| `message.delivered` · `message.read` · `message.failed` | WhatsApp lo entregó · el cliente lo leyó · no se pudo entregar |
| `conversation.created` | Empezó una conversación |
| `conversation.transferred` | Pasó a manos del equipo (`reason`: `agent`, `team`, `handoff`, `plan_limit`, `agent_error`) |
| `conversation.stage_changed` | Cambió de etapa (`change: { from, to, name, source }`) |
| `contact.created` | Contacto nuevo (`origin`: `whatsapp`, `panel`, `api`) |
| `order.created` · `order.status_changed` | Pedido nuevo · cambio de estado (`change: { from, to }`) |

## Cómo llega

```json theme={null}
{
  "id": "cmw8…",
  "event": "message.received",
  "created_at": "2026-11-15T15:04:05.000Z",
  "account": { "id": "cmw1…" },
  "data": {
    "conversation": { "id": "…", "status": "ai", "stage": "NUEVO", "started_at": "…" },
    "contact": { "id": "…", "phone": "573001234567", "name": "Ana" },
    "number": { "id": "…", "phone": "573009876543", "label": "Ventas" },
    "message": { "id": "…", "whatsapp_id": "wamid.…", "direction": "inbound", "from": "contact",
                 "type": "text", "text": "Hola, ¿tienen talla M?", "media_url": null, "status": "received" }
  }
}
```

En los pedidos, `data.order` trae `id`, `code`, `status`, `total`, `subtotal`, `delivery_fee`, `taxes`, `delivery`,
`address`, `items` (`product_id`, `name`, `price`, `quantity`…) y `created_at`, más `contact` y `conversation`.

Cabeceras: `X-Impulso-Event`, `X-Impulso-Delivery` (el `id` de la entrega, el mismo en cada reintento: úsalo para no
procesar dos veces) y `X-Impulso-Signature`.

## Verificar la firma

`X-Impulso-Signature: t=<unix>,v1=<hex>` — HMAC-SHA256 con tu `secret` sobre `"<t>.<cuerpo crudo>"`.

```javascript Node (Express) theme={null}
import crypto from 'node:crypto'
import express from 'express'

const app = express()
app.post('/impulso', express.raw({ type: 'application/json' }), (req, res) => {
  const [t, v1] = req.get('X-Impulso-Signature').split(',').map((p) => p.split('=')[1])
  const esperado = crypto.createHmac('sha256', process.env.IMPULSO_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex')
  const valida = v1.length === esperado.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado))
  if (!valida || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401)
  res.sendStatus(200) // responde rápido y procesa después
  const aviso = JSON.parse(req.body)
  // …
})
```

```python Python (Flask) theme={null}
import hmac, hashlib, os, time
from flask import Flask, request

app = Flask(__name__)

@app.post("/impulso")
def impulso():
    t, v1 = [p.split("=")[1] for p in request.headers["X-Impulso-Signature"].split(",")]
    cuerpo = request.get_data()
    esperado = hmac.new(os.environ["IMPULSO_WEBHOOK_SECRET"].encode(), f"{t}.".encode() + cuerpo, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(v1, esperado) or abs(time.time() - int(t)) > 300:
        return "", 401
    return "", 200
```

> [!WARNING]
>
> Verifica sobre el cuerpo **crudo**, tal como llegó (sin volver a convertir el JSON), y rechaza firmas de más de 5 minutos.

## Reintentos

Responde `2xx` en menos de 10 segundos. Si no, Impulso reintenta a 1 min, 5 min, 30 min, 2 h y 6 h; después la
entrega queda fallida (se puede reenviar desde el panel). Tras 20 fallos seguidos el aviso se apaga solo; reactívalo
con `PATCH /webhooks/{id}` (`active: true`). Con `POST /webhooks/{id}/test` envías un `ping` para probar.

Solo se aceptan direcciones `https` públicas. Hasta 10 avisos por cuenta.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.