Webhooks
Recibí eventos en tiempo real cuando algo pasa en un workspace que tu app integró: mensaje entrante, conversación creada, asignada, resuelta, etc. POSTs firmados con HMAC-SHA256.
Configuración
Cuando un workspace autoriza tu Connect App vía OAuth (flujo OAuth), IxiChat crea automáticamente un webhook saliente apuntando a <tu redirect_uri origin>/api/webhooks/ixichat. Los eventos a los que se suscribe son los que declaraste en webhook_events al registrar la app.
El webhook_secret viene en la respuesta de /api/connect/exchange. Guardalo — es lo que vas a usar para verificar las firmas.
Formato del request
IxiChat hace POST a tu URL con headers + body JSON:
POST https://miapp.com/api/webhooks/ixichat
Content-Type: application/json
X-IxiChat-Event: message.received
X-IxiChat-Delivery: <uuid>
X-IxiChat-Timestamp: 1716508800
X-IxiChat-Signature: t=1716508800,v1=<hex>
{
"event": "message.received",
"timestamp": "2026-05-23T20:00:00Z",
"tenant_id": "...",
"data": { ... }
}Verificación de firma
La firma es HMAC-SHA256(secret, timestamp + "." + body) en hex. Verificala SIEMPRE antes de procesar el evento — cualquiera puede mandar POSTs a tu URL.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyIxichatSignature(req: { headers: Headers; body: string }, secret: string): boolean {
const sig = req.headers.get("X-IxiChat-Signature") ?? "";
const match = sig.match(/t=(\d+),v1=([a-f0-9]+)/);
if (!match) return false;
const [, ts, hex] = match;
// Anti-replay: rechazar si el timestamp es muy viejo (>5 min)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(ts)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${ts}.${req.body}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(hex, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}No saltees el timestamp check
Eventos disponibles
| Event | Cuándo |
|---|---|
message.received | Un cliente final mandó un mensaje al tenant. |
message.sent | Un agente o el bot envió un mensaje saliente. |
conversation.created | Se abrió una nueva conversación. |
conversation.assigned | Cambió el agente / equipo asignado. |
conversation.resolved | Una conversación se marcó como resuelta. |
Payload por evento
message.received
{
"event": "message.received",
"timestamp": "...",
"tenant_id": "...",
"data": {
"message_id": "...",
"conversation_id": "...",
"contact": {
"id": "...",
"phone": "18091234567",
"name": "Manuel"
},
"channel": {
"id": "...",
"type": "whatsapp"
},
"content_type": "text",
"text": "Hola, necesito mi factura",
"media": null
}
}conversation.resolved
{
"event": "conversation.resolved",
"timestamp": "...",
"tenant_id": "...",
"data": {
"conversation_id": "...",
"status": "resolved",
"source": "embed" // o "manual" / "auto"
}
}Reintentos
- Si tu endpoint responde 2xx dentro de 10s: ok, evento entregado.
- Si responde 4xx: marcamos como permanente, no reintentamos. (Tu app rechazó intencionalmente.)
- Si responde 5xx o timeout: reintentamos con backoff exponencial — 30s, 2 min, 10 min, 1h, 6h. Después de 6h descartamos el evento.
Mejores prácticas
- Idempotencia: el mismo evento puede llegar 2+ veces (retry, network). Usá el header
X-IxiChat-Deliverycomo dedup key. - Respondé rápido: encolá el trabajo pesado a un worker en lugar de hacerlo dentro del handler del webhook. Devolvé 200 OK en milisegundos.
- Logueá las firmas que fallan: te ayuda a detectar abuse, secrets viejos sin rotar, o config rota.