Eventos (webhooks firmados)
POST firmado, con reintentos si tu servidor no responde. Así tu ERP, tu BI o tu sistema de cobranza se entera al instante sin consultar la API cada minuto.¿Y el nodo Llamar Webhook?
Las automatizaciones siguen pudiendo llamar a cualquier URL con el cuerpo que armes. Los eventos de esta página son distintos: un formato fijo y documentado, firmados, con reintentos e historial. Úsalos cuando otro sistema dependa de ellos.Configurar#
- Un administrador entra al CRM → Configuración → Integraciones, pestaña de productividad, sección Webhooks salientes.
- Agregar endpoint: la URL de tu servidor (tiene que ser
httpsy pública) y los eventos que quieres recibir. - Copia el secreto de firma (
whsec_…). Se muestra una sola vez; si lo pierdes, genera uno nuevo desde el mismo lugar. - Pulsa Probar: YAVE envía un evento
pingy te muestra qué respondió tu servidor.
Una organización puede tener hasta 10 endpoints, cada uno con sus propios eventos y su propio secreto.
Eventos#
| Evento | Cuándo | data |
|---|---|---|
lead.created | Entra un lead nuevo por cualquier canal (formulario, Meta, WhatsApp, API, manual). | { lead } |
lead.stage_changed | Un lead cambia de etapa, lo mueva un asesor, la IA, una automatización o la API. | { lead, from, to } |
lead.assigned | Se asigna o reasigna el asesor de un lead. | { lead, assignedTo } |
appointment.created | Se agenda una cita o visita. | { appointment } |
task.created | Se crea una tarea. | { task } |
lead y appointment tienen la misma forma que en la API de consulta. Un lead que ya existía y vuelve a escribir (reingreso) no genera lead.created.
Formato
{
"id": "evt_3f9a1c0b7d2e4a6f8b1c2d3e",
"type": "lead.stage_changed",
"createdAt": "2026-09-29T20:14:05.118Z",
"organizationId": "cm1…",
"data": {
"lead": {
"id": "cm9k2…",
"name": "Laura Gómez",
"email": "[email protected]",
"phone": "+573001234567",
"pipeline": { "id": "default", "name": "Ventas" },
"stage": { "id": "s-visita", "name": "Visita agendada" },
"assignedTo": { "id": "cm2u…", "name": "Carlos Ruiz", "email": "[email protected]" },
"tags": ["Torre 2"],
"createdAt": "2026-09-28T15:04:12.511Z",
"updatedAt": "2026-09-29T20:14:05.020Z"
},
"from": { "id": "CONTACTED", "name": "Contactado" },
"to": { "id": "s-visita", "name": "Visita agendada" }
}
}| Encabezado | Qué es |
|---|---|
X-Yave-Signature | Firma: t=<timestamp>,v1=<hmac>. Ver abajo. |
X-Yave-Event | El tipo de evento, igual que type. |
X-Yave-Event-Id | El id del evento. Es el mismo en todos los reintentos: úsalo para no procesar dos veces. |
X-Yave-Delivery-Id | El id de este envío (para soporte). |
X-Yave-Attempt | Número de intento, desde 1. |
Verificar la firma#
Cualquiera que conozca tu URL puede mandarle un POST. Verifica cada evento antes de confiar en él: calcula un HMAC-SHA256 con tu secreto sobre <t>.<cuerpo crudo> y compáralo con v1. Rechaza los que tengan un t de hace más de 5 minutos, para que nadie pueda repetir un evento viejo.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.YAVE_WEBHOOK_SECRET; // whsec_…
// Usa el cuerpo CRUDO: si lo parseas y lo vuelves a serializar, la firma no coincide.
app.post("/webhooks/yave", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Yave-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
const ok =
parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body);
// Responde rápido y procesa aparte; ignora ids que ya procesaste.
queue.push(event);
res.sendStatus(200);
});<?php
$secret = getenv('YAVE_WEBHOOK_SECRET'); // whsec_…
$payload = file_get_contents('php://input'); // cuerpo crudo
$header = $_SERVER['HTTP_X_YAVE_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$t = (int) ($parts['t'] ?? 0);
if (!$t || abs(time() - $t) > 300) { http_response_code(400); exit; }
$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);
if (!hash_equals($expected, $parts['v1'] ?? '')) { http_response_code(400); exit; }
$event = json_decode($payload, true);
// … guarda $event['id'] para no procesarlo dos veces
http_response_code(200);import hashlib, hmac, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["YAVE_WEBHOOK_SECRET"].encode() # whsec_…
@app.post("/webhooks/yave")
def yave():
payload = request.get_data() # cuerpo crudo (bytes)
parts = dict(p.split("=", 1) for p in request.headers.get("X-Yave-Signature", "").split(","))
t = int(parts.get("t", 0))
if not t or abs(time.time() - t) > 300:
abort(400)
expected = hmac.new(SECRET, f"{t}.".encode() + payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = json.loads(payload)
# … encola event y deduplica por event["id"]
return "", 200Respuestas y reintentos#
- Responde 2xx en menos de 10 segundos. Cualquier otra respuesta, un timeout o un error de conexión cuenta como fallo. Si tienes que hacer algo lento, encólalo y responde de una vez.
- Si falla, YAVE reintenta a los 1 min, 5 min, 30 min, 2 h, 6 h, 12 h y 24 h: 8 intentos en total, durante casi dos días.
- No se siguen redirecciones: registra la URL final.
- Entrega al menos una vez: un evento puede llegar repetido (por ejemplo, si tu servidor lo procesó pero la respuesta no llegó). Deduplica por
X-Yave-Event-Id. - El orden no está garantizado: un reintento puede llegar después de un evento más nuevo. Usa
createdAtdel evento olead.updatedAtpara quedarte con el estado más reciente, o consulta el lead en la API de consulta. - Si 20 eventos seguidos agotan sus reintentos, el endpoint se desactiva solo y el CRM muestra el motivo. Arregla tu servidor y vuelve a activarlo; los eventos fallidos se pueden reenviar desde el historial.
Historial y reenvíos#
En el CRM, cada endpoint tiene un Historial con los últimos envíos: evento, estado, código HTTP, intentos y el error, si hubo. Un envío fallido se puede Reenviar después de corregir tu servidor. El historial se guarda 30 días.
Seguridad#
- Solo
https. YAVE no envía a direcciones internas (localhost, redes privadas, metadatos de nube), ni siquiera si un nombre público resuelve a una. - Guarda el secreto como una contraseña. Si se filtra, pulsa Nuevo secreto: el anterior deja de servir de inmediato.
- Los eventos llevan datos de contacto de personas. Trátalos con el mismo cuidado que tu base de clientes.