Webhooks
En lugar de preguntarle a la API cada tanto si algo cambió, registrá una dirección de tu servidor y Kaptativa le manda un POST firmado cuando pasa algo en la cuenta.
Registrar un destino
- En el panel, abrí Administración > API y MCP y tocá el botón Webhooks.
- En Agregar un destino, cargá un nombre, la dirección
httpsde tu servidor y los eventos que querés recibir, y tocá Agregar el destino. - Copiá el secreto de firma, que empieza con
whsec_. Se muestra una sola vez. - Tocá Probar en el destino: manda un evento
webhook.pruebapara que compruebes la firma antes de recibir eventos reales. El resultado aparece en el Registro de entregas unos segundos después, con Actualizar.
- La dirección tiene que ser
httpsy pública, de hasta 500 caracteres y sin usuario ni contraseña adentro. Si lleva puerto, tiene que ser el 443. No se aceptan direcciones internas, las de la propia plataforma ni las de los sitios de las cuentas, y se vuelve a comprobar en cada envío. - Cada cuenta puede tener hasta 10 destinos.
- El secreto se cambia desde el panel, con Cambiar el secreto. El anterior deja de valer en el momento: cargá el nuevo en tu servidor enseguida.
- Un destino recibe un evento solo si la cuenta tiene activo el módulo del evento y la persona que creó el destino todavía puede ver esos datos. Al crearlo se pueden elegir solo los eventos que esa persona ve.
- Si otra persona de la cuenta cambia la dirección, los eventos o el secreto de un destino, el destino pasa a su nombre y desde ahí recibe lo que esa persona puede ver. Con el cambio de nombre cambia también el secreto: el anterior deja de valer en el momento y el nuevo se le muestra una sola vez a quien hizo el cambio, que lo tiene que cargar en el servidor que recibe.
- Un destino nuevo que falla 5 envíos seguidos sin haber recibido nunca uno bien queda suspendido. Probalo con Probar antes de dejarlo andando.
- Con Pausar, lo que pase mientras tanto no se manda después; lo que ya estaba pendiente sale al tocar Reanudar.
- Una cuenta suspendida no manda avisos: lo pendiente queda como fallido.
Lo que llega
Un POST con el cuerpo en JSON. La forma de afuera es igual para todos los eventos; lo que cambia es datos.
{
"id": "0b6f2a9c-4d1e-4f7a-8b3c-5d6e7f8a9b0c",
"tipo": "inmobiliaria.propiedad.publicada",
"version": 1,
"creado": "2026-11-11T15:04:05.000Z",
"datos": {
"propiedad_id": "c0a8012e-7b1f-4c3a-9d2e-5f6a7b8c9d0e",
"codigo": null,
"publicada": true,
"motivo": null
}
}| Campo | Qué trae |
|---|---|
id | El identificador del evento. No cambia entre reintentos ni en un reenvío: sirve para descartar repetidos. |
tipo | El nombre del evento, de la lista de abajo. |
version | La versión de la forma de datos para ese tipo. Hoy, 1. |
creado | Cuándo pasó, en ISO 8601 y en UTC. |
datos | Lo propio del evento. Trae identificadores y lo que cambió, no la ficha completa. |
Un evento no trae nombres de personas, emails ni teléfonos: trae identificadores y lo que cambió. Con ese id pedís el registro por la API, con los alcances y permisos de tu token.
El único texto libre que viaja es el título de la tarjeta (el campo titulo), en gestion.tarjeta.creada y gestion.tarjeta.movida. Si en los títulos de las tarjetas se escriben datos de personas, salen en esos avisos.
Encabezados
| Encabezado | Qué trae |
|---|---|
webhook-id | El mismo valor que id en el cuerpo. |
webhook-timestamp | El momento del envío, en segundos desde 1970. Cambia en cada intento. |
webhook-signature | La firma, como v1, seguido de la firma en base64. |
Verificar la firma
Cualquiera que conozca tu dirección puede mandarle un POST. La firma prueba que lo mandó Kaptativa y que nadie tocó el cuerpo. Sigue el formato de Standard Webhooks, así que también podés usar una de sus bibliotecas.
- Armá el texto firmado uniendo con puntos el
webhook-id, elwebhook-timestampy el cuerpo tal como llegó, sin pasarlo por un lector de JSON. - Calculá el HMAC-SHA256 de ese texto. La clave son los bytes del secreto: lo que viene después de
whsec_, decodificado de base64. - Compará el resultado en base64 con lo que viene después de
v1,, en tiempo constante. - Rechazá el pedido si el
webhook-timestamptiene más de cinco minutos de diferencia con tu reloj: evita que alguien repita un envío viejo.
import { createHmac, timingSafeEqual } from "node:crypto";
// cuerpo: el texto crudo del pedido, sin volver a armarlo. secreto: el whsec_ del destino
export function firmaValida(encabezados, cuerpo, secreto) {
const id = encabezados["webhook-id"];
const momento = encabezados["webhook-timestamp"];
const firmas = encabezados["webhook-signature"] ?? "";
// Un aviso de hace más de cinco minutos no se acepta
if (!id || !momento || Math.abs(Date.now() / 1000 - Number(momento)) > 300) return false;
// La clave son los bytes del secreto: lo que viene después de whsec_, decodificado de base64
const clave = Buffer.from(secreto.replace(/^whsec_/, ""), "base64");
const esperada = createHmac("sha256", clave).update(`${id}.${momento}.${cuerpo}`).digest();
// Puede venir más de una firma, separadas por espacios: alcanza con que una coincida
return firmas.split(" ").some((f) => {
const recibida = Buffer.from(f.replace(/^v1,/, ""), "base64");
return recibida.length === esperada.length && timingSafeEqual(recibida, esperada);
});
}import express from "express";
const app = express();
// express.raw deja el cuerpo sin tocar, que es lo que se firmó
app.post("/webhooks/kaptativa", express.raw({ type: "application/json" }), (pedido, respuesta) => {
const cuerpo = pedido.body.toString("utf8");
if (!firmaValida(pedido.headers, cuerpo, process.env.KAPTATIVA_WEBHOOK_SECRETO)) {
return respuesta.status(401).end();
}
const evento = JSON.parse(cuerpo);
// Respondé ya y procesá después: encolá el evento con su id
respuesta.status(204).end();
});Guardalo en una variable de entorno o en tu gestor de secretos, como el token de la API. Si se filtra, rotalo desde el panel.
Cómo responder
- Respondé con un código
2xxantes de 10 segundos. Cualquier otra cosa cuenta como falla y se reintenta. - Una redirección (
3xx) también cuenta como falla: no se sigue. Registrá la dirección final. - Respondé rápido y procesá después. Si el trabajo es largo, guardá el evento en una cola y contestá
204. - Un
410le dice a Kaptativa que esa dirección ya no existe: el destino queda suspendido sin más reintentos.
Reintentos
Si tu servidor no responde bien, el mismo evento se manda hasta 7 veces en total, con el mismo webhook-id y el mismo cuerpo:
| Intento | Cuándo |
|---|---|
| 1 | En el momento |
| 2 | 1 minuto después del anterior |
| 3 | 5 minutos después del anterior |
| 4 | 30 minutos después del anterior |
| 5 | 2 horas después del anterior |
| 6 | 6 horas después del anterior |
| 7 | 24 horas después del anterior |
- Después del último intento la entrega queda como Fallida. Desde el panel se puede mandar de nuevo con Reenviar, en el Registro de entregas: llega idéntica, con el mismo
id. - Con 5 entregas agotadas seguidas, el destino queda Suspendido: lo que tenía pendiente queda fallido, no se le manda nada nuevo y quienes ven API y MCP reciben un aviso en el panel y por correo. Vuelve a recibir con Reanudar.
- El panel guarda 30 días del registro de entregas, con el código y el comienzo de lo que respondió tu servidor.
Un reintento o un reenvío hacen que el mismo evento llegue más de una vez, y uno que falló puede llegar después de otro más nuevo. Guardá los id que ya procesaste para descartar repetidos, y si el orden importa pedí el estado actual por la API en lugar de confiar en el orden de llegada.
Eventos
Estos son los 18 eventos que se pueden elegir en un destino. El esquema completo de cada uno está en la sección webhooks de openapi.json.
Inmobiliaria
| Evento | Cuándo se manda | Campos de datos |
|---|---|---|
inmobiliaria.propiedad.publicada | Cuando una propiedad se publica en el sitio o deja de estarlo. publicada dice cuál de las dos. | propiedad_idcodigopublicadamotivo |
inmobiliaria.propiedad.precio_cambiado | Cuando cambia el precio en pesos o en dólares de una propiedad. | propiedad_idcodigoprecio_arsprecio_usd |
inmobiliaria.propiedad.estado_cambiado | Cuando una propiedad pasa de un estado a otro: disponible, reservada, vendida, alquilada. | propiedad_idcodigoantesdespuesoperacion |
inmobiliaria.consulta.recibida | Cuando entra una consulta por una propiedad o un tema. Los datos de quien escribió se piden por la API. | consulta_idsobrecanal |
inmobiliaria.consulta.cerrada | Cuando alguien del equipo cierra una consulta, con su resultado. | consulta_idresultado |
Tienda
| Evento | Cuándo se manda | Campos de datos |
|---|---|---|
tienda.pedido.creado | Cuando se carga un pedido, desde el panel o desde el sitio, con sus productos. | pedido_idcodigocliente_idcanalitems |
tienda.pedido.pago_confirmado | Cuando se acredita un pago. pago_completo dice si con este pago el pedido quedó saldado. | pedido_idcodigocliente_idcanalimporte_pagopagado_acumuladosaldopago_completo |
tienda.pedido.pago_revertido | Cuando se da marcha atrás con un pago que estaba acreditado. | pedido_idcodigocliente_idcanalimporte_pagopagado_acumuladosaldo |
tienda.pedido.cancelado | Cuando un pedido se cancela, vence sin pagarse, se elimina o el cliente se arrepiente. | pedido_idcodigocliente_idcanal |
Turnos
| Evento | Cuándo se manda | Campos de datos |
|---|---|---|
turnos.turno.creado | Cuando se crea un turno, con una o varias sesiones, desde el panel, el sitio o una compra. | turno_idcodigocliente_idservicio_idsesionescanalpedido_id |
turnos.sesion.agendada | Cuando una sesión de un turno queda con día y hora. | sesion_idturno_idcliente_idiniciocanal |
turnos.sesion.cancelada | Cuando una sesión se cancela o se saca de la agenda. por_agendar dice si queda para darle otro horario. | sesion_idturno_idcliente_idiniciopor_agendar |
Sitio
| Evento | Cuándo se manda | Campos de datos |
|---|---|---|
sitio.respuesta_formulario.recibida | Cuando alguien completa un formulario del sitio. Lo que respondió se pide por la API. | respuesta_idformulario_idformularionumerocliente_id |
Gestión
| Evento | Cuándo se manda | Campos de datos |
|---|---|---|
gestion.tarjeta.creada | Cuando se agrega una tarjeta a un tablero que quien creó el destino puede ver. | tarjeta_idtablero_idtitulocolumnacopia_de |
gestion.tarjeta.movida | Cuando una tarjeta pasa de una columna a otra. | tarjeta_idtablero_idtitulodea |
gestion.proveedor.creado | Cuando se da de alta un proveedor. Sus datos se piden por la API. | proveedor_id |
Alquileres
| Evento | Cuándo se manda | Campos de datos |
|---|---|---|
alquileres.pago.registrado | Cuando se registra un cobro de un contrato, con su recibo. | pago_idrecibomontomonedacontrato |
alquileres.mensualidad.generada | Cuando se genera la mensualidad de un contrato para un período. | mensualidad_idcontratoperiodo |
Además, webhook.prueba es el evento que manda el botón Probar. No viene de la cuenta y lo recibe cualquier destino, haya elegido los eventos que haya elegido.
La lista crece con el tiempo y a un evento se le pueden sumar campos. Ignorá los tipos y los campos que tu código no conoce en lugar de fallar.
Relacionado
- Referencia de recursos, para pedir el registro completo a partir del id del evento.
- Registro de cambios, donde se anuncian los eventos nuevos.