Ir al contenido

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

  1. En el panel, abrí Administración > API y MCP y tocá el botón Webhooks.
  2. En Agregar un destino, cargá un nombre, la dirección https de tu servidor y los eventos que querés recibir, y tocá Agregar el destino.
  3. Copiá el secreto de firma, que empieza con whsec_. Se muestra una sola vez.
  4. Tocá Probar en el destino: manda un evento webhook.prueba para 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 https y 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.

Cuerpo de inmobiliaria.propiedad.publicada
{
  "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
  }
}
Campos del cuerpo
CampoQué trae
idEl identificador del evento. No cambia entre reintentos ni en un reenvío: sirve para descartar repetidos.
tipoEl nombre del evento, de la lista de abajo.
versionLa versión de la forma de datos para ese tipo. Hoy, 1.
creadoCuándo pasó, en ISO 8601 y en UTC.
datosLo propio del evento. Trae identificadores y lo que cambió, no la ficha completa.
Los datos personales se piden por la API

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

Encabezados del pedido
EncabezadoQué trae
webhook-idEl mismo valor que id en el cuerpo.
webhook-timestampEl momento del envío, en segundos desde 1970. Cambia en cada intento.
webhook-signatureLa 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.

  1. Armá el texto firmado uniendo con puntos el webhook-id, el webhook-timestamp y el cuerpo tal como llegó, sin pasarlo por un lector de JSON.
  2. Calculá el HMAC-SHA256 de ese texto. La clave son los bytes del secreto: lo que viene después de whsec_, decodificado de base64.
  3. Compará el resultado en base64 con lo que viene después de v1,, en tiempo constante.
  4. Rechazá el pedido si el webhook-timestamp tiene más de cinco minutos de diferencia con tu reloj: evita que alguien repita un envío viejo.
Verificación en Node.js, sin dependencias
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);
  });
}
Usarla en Express
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();
});
El secreto no va en el código

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 2xx antes 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 410 le 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:

Calendario de reintentos
IntentoCuándo
1En el momento
21 minuto después del anterior
35 minutos después del anterior
430 minutos después del anterior
52 horas después del anterior
66 horas después del anterior
724 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.
Preparate para repetidos y para el desorden

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

Eventos de Inmobiliaria
EventoCuándo se mandaCampos de datos
inmobiliaria.propiedad.publicadaCuando una propiedad se publica en el sitio o deja de estarlo. publicada dice cuál de las dos.propiedad_idcodigopublicadamotivo
inmobiliaria.propiedad.precio_cambiadoCuando cambia el precio en pesos o en dólares de una propiedad.propiedad_idcodigoprecio_arsprecio_usd
inmobiliaria.propiedad.estado_cambiadoCuando una propiedad pasa de un estado a otro: disponible, reservada, vendida, alquilada.propiedad_idcodigoantesdespuesoperacion
inmobiliaria.consulta.recibidaCuando entra una consulta por una propiedad o un tema. Los datos de quien escribió se piden por la API.consulta_idsobrecanal
inmobiliaria.consulta.cerradaCuando alguien del equipo cierra una consulta, con su resultado.consulta_idresultado

Tienda

Eventos de Tienda
EventoCuándo se mandaCampos de datos
tienda.pedido.creadoCuando se carga un pedido, desde el panel o desde el sitio, con sus productos.pedido_idcodigocliente_idcanalitems
tienda.pedido.pago_confirmadoCuando se acredita un pago. pago_completo dice si con este pago el pedido quedó saldado.pedido_idcodigocliente_idcanalimporte_pagopagado_acumuladosaldopago_completo
tienda.pedido.pago_revertidoCuando se da marcha atrás con un pago que estaba acreditado.pedido_idcodigocliente_idcanalimporte_pagopagado_acumuladosaldo
tienda.pedido.canceladoCuando un pedido se cancela, vence sin pagarse, se elimina o el cliente se arrepiente.pedido_idcodigocliente_idcanal

Turnos

Eventos de Turnos
EventoCuándo se mandaCampos de datos
turnos.turno.creadoCuando se crea un turno, con una o varias sesiones, desde el panel, el sitio o una compra.turno_idcodigocliente_idservicio_idsesionescanalpedido_id
turnos.sesion.agendadaCuando una sesión de un turno queda con día y hora.sesion_idturno_idcliente_idiniciocanal
turnos.sesion.canceladaCuando 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

Eventos de Sitio
EventoCuándo se mandaCampos de datos
sitio.respuesta_formulario.recibidaCuando alguien completa un formulario del sitio. Lo que respondió se pide por la API.respuesta_idformulario_idformularionumerocliente_id

Gestión

Eventos de Gestión
EventoCuándo se mandaCampos de datos
gestion.tarjeta.creadaCuando se agrega una tarjeta a un tablero que quien creó el destino puede ver.tarjeta_idtablero_idtitulocolumnacopia_de
gestion.tarjeta.movidaCuando una tarjeta pasa de una columna a otra.tarjeta_idtablero_idtitulodea
gestion.proveedor.creadoCuando se da de alta un proveedor. Sus datos se piden por la API.proveedor_id

Alquileres

Eventos de Alquileres
EventoCuándo se mandaCampos de datos
alquileres.pago.registradoCuando se registra un cobro de un contrato, con su recibo.pago_idrecibomontomonedacontrato
alquileres.mensualidad.generadaCuando 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