Guías
Webhooks
Avisos firmados cuando algo pasa: eventos, firma HMAC y reintentos.
Configurar
En el panel, Integraciones → Nuevo webhook: la dirección de tu servidor (https) y los eventos que quieres recibir. Al crearlo se muestra una sola vez su secreto, con el que verificas que cada aviso viene de Simpligastos. Guárdalo como guardas la llave.
La dirección tiene que ser pública: no se aceptan direcciones de redes internas, y Simpligastos no sigue redirecciones.
El aviso
En el momento en que pasa un evento suscrito, Simpligastos manda un POST con JSON. El aviso es liviano: trae el evento, la entidad y su id; el detalle lo pides con la API.
{
"evento": "rendicion.aprobada",
"version": "1",
"entidad": "rendicion",
"id": "3e8bcc2a-85b8-49a0-8fa2-5339c8936d93",
"detalle": { "comentario": "Ok" },
"ocurrido_en": "2026-10-04T12:02:11.482391-03:00",
"envio": "b1f0c9a2-2c1e-4c8a-9a51-0e3c4f5a6b7c"
}Simpligastos-Evento- El evento, por ejemplo rendicion.aprobada.
Simpligastos-Envio- El id del envío. Es la llave de idempotencia: el mismo envío puede llegar dos veces.
Simpligastos-Firma- t=<segundos>,v1=<firma>: la firma HMAC-SHA256 del aviso con el secreto del webhook.
User-Agent- Simpligastos-Webhooks/1
Verificar la firma
La firma es el HMAC-SHA256, en hexadecimal, de t, un punto y el cuerpo tal como llegó, con el secreto del webhook. Calcúlala sobre el cuerpo crudo, antes de interpretarlo como JSON: si lo vuelves a serializar, cambia y la firma no calza. Compara en tiempo constante y descarta los avisos de más de 5 minutos.
import { createHmac, timingSafeEqual } from "node:crypto";
// cuerpo: el texto tal como llegó, antes de interpretarlo como JSON.
function verificar(cuerpo, encabezado, secreto) {
const { t, v1 } = Object.fromEntries(encabezado.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // más de 5 minutos: repetido
const esperado = Buffer.from(createHmac("sha256", secreto).update(`${t}.${cuerpo}`).digest("hex"));
const recibido = Buffer.from(v1 ?? "");
return recibido.length === esperado.length && timingSafeEqual(esperado, recibido);
}import hashlib
import hmac
import time
# cuerpo: los bytes tal como llegaron, antes de interpretarlos como JSON.
def verificar(cuerpo: bytes, encabezado: str, secreto: str) -> bool:
partes = dict(p.split("=", 1) for p in encabezado.split(",") if "=" in p)
t, v1 = partes.get("t", "0"), partes.get("v1", "")
if abs(time.time() - int(t)) > 300: # más de 5 minutos: repetido
return False
firmado = t.encode() + b"." + cuerpo
esperado = hmac.new(secreto.encode(), firmado, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado, v1)Un receptor mínimo en Node, sin dependencias:
import { createServer } from "node:http";
createServer(async (pedido, respuesta) => {
let cuerpo = "";
for await (const parte of pedido) cuerpo += parte;
if (!verificar(cuerpo, pedido.headers["simpligastos-firma"] ?? "", process.env.SIMPLIGASTOS_SECRETO)) {
respuesta.writeHead(400).end();
return;
}
const aviso = JSON.parse(cuerpo);
// Responde rápido y procesa aparte: el mismo envío puede llegar dos veces.
respuesta.writeHead(204).end();
procesar(aviso); // usa aviso.envio para no procesarlo dos veces
}).listen(3000);Respuesta y reintentos
Responde cualquier 2xx en menos de 5 segundos; si tienes que hacer algo largo, responde primero y procésalo aparte. Si no responde a tiempo o responde otra cosa, se reintenta, esperando entre un intento y el siguiente 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas, 12 horas. Pasadas 24 horas desde el evento, queda como fallido.
En el panel ves cada envío con su respuesta y puedes reenviarlo. Como un aviso puede llegar más de una vez, guarda el Simpligastos-Envio que ya procesaste y descarta los repetidos.
Eventos
Cada evento es entidad.acción. Eliges cuáles recibir en cada webhook.
gasto
gasto.creadogasto.leidogasto.corregido
rendicion
rendicion.creadarendicion.enviadarendicion.paso_aprobadorendicion.aprobadarendicion.aprobada_solarendicion.aprobada_automaticarendicion.rechazadarendicion.cerradarendicion.contabilizada
persona
persona.invitadapersona.invitacion_aceptadapersona.modificadapersona.desactivada
fondo
fondo.creadofondo.abonadofondo.cerrado
solicitud_fondo
solicitud_fondo.pedidasolicitud_fondo.aprobadasolicitud_fondo.rechazada
pago
pago.pagadopago.anulado
nomina
nomina.generadanomina.pagada