Plataforma y desarrolladores
Webhooks firmados
Cómo envía tu sistema leads y eventos a Guren AI y cómo recibe las acciones de Ana, con firma HMAC-SHA256, protección contra repetición e idempotencia.
Si tu empresa tiene un sistema propio (un CRM a medida, un bot, un ERP), se conecta con Ana en ambas direcciones con dos contratos simétricos: el ingreso firmado, para que tu sistema nos envíe leads y eventos, y el webhook saliente, para que tu sistema reciba lo que Ana hace. Los dos usan el mismo esquema de firma.
#El esquema de firma
Cada solicitud lleva dos encabezados. La firma es un HMAC-SHA256, en hexadecimal, del timestamp, un punto y el cuerpo crudo de la solicitud, con un secreto que se comparte una sola vez y por un canal seguro durante la implementación.
| Encabezado | Contenido |
|---|---|
X-Guren-Timestamp | Segundos desde 1970 (Unix) al momento de firmar. |
X-Guren-Signature | sha256= seguido de hex(HMAC_SHA256(secreto, timestamp + "." + cuerpo)). |
X-Guren-Event | Solo en el webhook saliente: la acción que se informa. |
#Ingreso firmado: de tu sistema a Ana
Envía uno o varios eventos en una misma solicitud a la URL de tu instancia, que te entregamos en la implementación.
/<instancia>">
POST https://guren.cl/api/v1/hooks/clinica-ejemplo/ingest
Content-Type: application/json
X-Guren-Timestamp: 1793019740
X-Guren-Signature: sha256=5f2c…e91a
{
"events": [
{
"id": "crm-123",
"type": "lead.flow_abandoned",
"contact": { "phone": "+56912345678", "name": "Pedro Soto", "externalId": "123" },
"text": "Abandonó la agenda en la pregunta 2",
"data": { "consent": true, "producto": "Evaluación inicial" }
}
]
}| Campo | Obligatorio | Notas |
|---|---|---|
id | Sí | Identificador estable del evento en tu sistema. Si reintentas con el mismo id, no se procesa dos veces. |
type | Sí | Por defecto se aceptan lead.created, lead.updated, lead.flow_abandoned, meeting.booked y custom. |
contact | Sí | Al menos uno de phone (con código de país), email o externalId. |
text | No | Contexto en lenguaje natural para Ana. |
data | No | Datos adicionales. consent: true declara que la persona aceptó ser contactada. |
{ "ok": true, "accepted": 1, "rejected": [] }Si algún evento no es válido, se informa en rejected con su posición y el motivo, y el resto se procesa. Si ninguno es válido, la respuesta es 400; si la firma no es válida, 401.
#Firmar desde tu sistema
import { createHmac } from 'node:crypto';
export async function enviarAGuren(url, secreto, eventos) {
const cuerpo = JSON.stringify({ events: eventos });
const ts = Math.floor(Date.now() / 1000).toString();
const firma = 'sha256=' + createHmac('sha256', secreto).update(`${ts}.${cuerpo}`).digest('hex');
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Guren-Timestamp': ts, 'X-Guren-Signature': firma },
body: cuerpo // exactamente el mismo texto que se firmó
});
return res.json();
}import hashlib, hmac, json, time, urllib.request
def enviar_a_guren(url: str, secreto: str, eventos: list) -> dict:
cuerpo = json.dumps({"events": eventos}, separators=(",", ":")).encode()
ts = str(int(time.time()))
firma = "sha256=" + hmac.new(secreto.encode(), f"{ts}.".encode() + cuerpo, hashlib.sha256).hexdigest()
req = urllib.request.Request(url, data=cuerpo, method="POST", headers={
"Content-Type": "application/json", "X-Guren-Timestamp": ts, "X-Guren-Signature": firma})
with urllib.request.urlopen(req, timeout=10) as r:
return json.load(r)#Webhook saliente: de Ana a tu sistema
Cuando Ana crea o actualiza un contacto, registra una actividad (con la transcripción o el resumen), cambia un estado o avisa algo a tu equipo, lo publica como un POST firmado a la URL que nos indiques.
POST https://tu-sistema.cl/guren/webhook
Content-Type: application/json
X-Guren-Timestamp: 1793019801
X-Guren-Signature: sha256=a81d…07bc
X-Guren-Event: crm.log_activity
{
"tenant": "clinica-ejemplo",
"action": "crm.log_activity",
"payload": { "contact": { "phone": "+56912345678", "name": "Pedro Soto" }, "channel": "whatsapp", "text": "Agendó evaluación para el jueves 10:30" },
"sentAt": "2026-11-02T15:03:21.000Z"
}X-Guren-Event | Cuándo llega |
|---|---|
crm.upsert_contact | Un contacto nuevo o con datos nuevos (nombre, correo, teléfono). |
crm.log_activity | Una conversación, una llamada con su resumen, una reunión agendada. |
crm.set_status | Cambio de estado: calificado, agendado, no interesado, baja. |
notify.post | Un aviso para tu equipo. |
Si tu sistema responde {"ref": "<id en tu sistema>"}, Ana guarda esa referencia y la incluye en los siguientes eventos del mismo contacto, para que actualices el registro correcto sin buscarlo.
#Verificar en tu servidor
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
const SECRETO = process.env.GUREN_SIGNING_SECRET;
// Importante: leer el cuerpo CRUDO; la firma se calcula sobre el texto exacto
app.post('/guren/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Guren-Timestamp') || '';
const recibida = req.get('X-Guren-Signature') || '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const esperada = 'sha256=' + createHmac('sha256', SECRETO).update(`${ts}.${req.body}`).digest('hex');
const a = Buffer.from(esperada), b = Buffer.from(recibida);
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString('utf8'));
res.status(200).json({ ref: guardar(evento) }); // responde rápido; procesa lo pesado después
});#Buenas prácticas
- Verifica con el cuerpo crudo. Si parseas y vuelves a serializar el JSON, la firma deja de coincidir.
- Compara en tiempo constante (
timingSafeEqualohmac.compare_digest), nunca con==. - Responde rápido. Devuelve
200en cuanto validas la firma y procesa lo pesado en segundo plano. - Sé idempotente. Un mismo evento puede llegar más de una vez; usa el identificador del contacto y la acción para no duplicar registros.
- Rota el secreto si sospechas que se expuso. Coordinamos la rotación contigo para no perder eventos.