Webhooks
Rimasun avisa a su servidor lo que pasa en las salas. Verifique siempre la firma antes de creerle al aviso.
Configurar
En Mi cuenta → Proyecto → Webhooks ponga la URL (https) de su servidor y copie el secreto de webhooks
(whs_…). Con «Enviar prueba» llega un evento de tipo prueba.
Eventos
Un POST JSON por evento:
{ "id": "evt_…", "tipo": "participante.entro", "proyecto": "prj_…", "fecha": 1792448317,
"datos": { "sala": "cita-250", "identidad": "paciente-174", "nombre": "Ana" } }
| Tipo | Cuándo |
|---|---|
sala.iniciada | Entró la primera persona a la sala. |
sala.terminada | La sala se cerró (vacía, cerrada por API o por duración máxima). |
participante.entro | Una identidad se conectó. |
participante.salio | Una identidad se desconectó. |
prueba | Enviado desde la consola para probar. |
Cabeceras: X-Rimasun-Evento (el tipo), X-Rimasun-Firma y User-Agent: Rimasun-Webhook/1.
Si su servidor no responde 2xx en 8 segundos, se reintenta a los 2 s, 10 s y 30 s: use id para no procesar dos veces.
Verificar la firma
X-Rimasun-Firma: t=1792448317,v1=5f2b…(64 hex)
v1 = HMAC-SHA256(secreto_webhook, t + "." + cuerpo_crudo) → en hexadecimal
- Lea el cuerpo crudo (bytes tal como llegaron). No lo vuelva a serializar desde el JSON.
- Rechace si
ttiene más de 5 minutos de diferencia con su reloj (evita reenvíos). - Calcule el HMAC y compárelo en tiempo constante.
- Responda
200rápido y procese después.
const crypto = require("node:crypto");
function verificarFirma(cuerpo, cabecera, secreto) {
const p = Object.fromEntries(String(cabecera || "").split(",").map((x) => x.trim().split("=", 2)));
const t = Number(p.t);
if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(p.v1 || "")) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const esperado = crypto.createHmac("sha256", secreto).update(t + ".").update(cuerpo).digest();
const dado = Buffer.from(p.v1, "hex");
return dado.length === esperado.length && crypto.timingSafeEqual(dado, esperado);
}
// Express: app.post("/rimasun/webhook", express.raw({ type: "application/json" }), (req, res) => {
// if (!verificarFirma(req.body, req.get("X-Rimasun-Firma"), process.env.RIMASUN_WEBHOOK_SECRETO)) return res.sendStatus(401);
// const evento = JSON.parse(req.body); res.sendStatus(200); … });function rimasun_verificar_firma(string $cuerpo, string $cabecera, string $secreto): bool {
$p = [];
foreach (explode(',', $cabecera) as $x) { $kv = explode('=', trim($x), 2); if (count($kv) === 2) $p[$kv[0]] = $kv[1]; }
if (!isset($p['t'], $p['v1']) || !ctype_digit($p['t']) || !preg_match('/^[0-9a-f]{64}$/', $p['v1'])) return false;
if (abs(time() - (int)$p['t']) > 300) return false;
return hash_equals(hash_hmac('sha256', $p['t'] . '.' . $cuerpo, $secreto), $p['v1']);
}
$cuerpo = file_get_contents('php://input');
if (!rimasun_verificar_firma($cuerpo, $_SERVER['HTTP_X_RIMASUN_FIRMA'] ?? '', getenv('RIMASUN_WEBHOOK_SECRETO'))) {
http_response_code(401); exit;
}
$evento = json_decode($cuerpo, true);import hashlib, hmac, re, time
def verificar_firma(cuerpo: bytes, cabecera: str, secreto: str) -> bool:
p = dict(x.strip().split("=", 1) for x in (cabecera or "").split(",") if "=" in x)
t, v1 = p.get("t", ""), p.get("v1", "")
if not t.isdigit() or not re.fullmatch(r"[0-9a-f]{64}", v1):
return False
if abs(time.time() - int(t)) > 300:
return False
esperado = hmac.new(secreto.encode(), t.encode() + b"." + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, v1)
# Flask: verificar_firma(request.get_data(), request.headers.get("X-Rimasun-Firma"), secreto)
# Django: verificar_firma(request.body, request.headers.get("X-Rimasun-Firma"), secreto)Si el secreto de webhooks se filtra, genere uno nuevo en la consola: el anterior deja de valer al instante.
Rimasun · documentación en español · cambios por versión