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:

text
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.

ts
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

Sin la verificación del timestamp, un atacante que interceptó UN request puede replayarlo cuando quiera. 5 min de tolerancia es el estándar.

Eventos disponibles

EventCuándo
message.receivedUn cliente final mandó un mensaje al tenant.
message.sentUn agente o el bot envió un mensaje saliente.
conversation.createdSe abrió una nueva conversación.
conversation.assignedCambió el agente / equipo asignado.
conversation.resolvedUna conversación se marcó como resuelta.

Payload por evento

message.received

json
{
  "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

json
{
  "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-Delivery como 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.