Rimasun Docs

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" } }
TipoCuándo
sala.iniciadaEntró la primera persona a la sala.
sala.terminadaLa sala se cerró (vacía, cerrada por API o por duración máxima).
participante.entroUna identidad se conectó.
participante.salioUna identidad se desconectó.
pruebaEnviado 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
  1. Lea el cuerpo crudo (bytes tal como llegaron). No lo vuelva a serializar desde el JSON.
  2. Rechace si t tiene más de 5 minutos de diferencia con su reloj (evita reenvíos).
  3. Calcule el HMAC y compárelo en tiempo constante.
  4. Responda 200 rá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