Integración
Webhooks
Kuida avisa a su sistema lo que pasa del lado de Kuida —una solicitud de ingreso completa, un evento procesado— con un POST firmado a una URL suya.
Registrar un endpoint
Un endpoint es una URL https de su sistema y la lista de eventos que quiere recibir (* recibe todos). La respuesta de creación trae el secret con el que se verifica la firma: se muestra una sola vez, guárdelo junto a la clave de API.
curl https://www.kuida.ar/api/v1/webhook_endpoints \
-H "Authorization: Bearer kd_test_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://sistema.example/kuida/webhooks",
"enabledEvents": [
"intake.ready",
"intake.created"
],
"description": "Sistema de admisión"
}'{
"id": "we_cm1a2b3c4d5e6f7g8h9i0j",
"object": "webhook_endpoint",
"url": "https://sistema.example/kuida/webhooks",
"description": "Sistema de admisión",
"enabledEvents": [
"intake.ready",
"intake.created"
],
"status": "enabled",
"secret": "whsec_…",
"lastDeliveredAt": null,
"lastError": null,
"createdAt": "2026-10-01T17:30:00.000Z"
}Los endpoints se administran con las operaciones de Webhooks de la referencia: listar, cambiar la URL o los eventos, pausar con disabled: true y borrar.
Eventos disponibles
intake.requested- Cuándo se envíaAlguien pide el servicio para un paciente. Abre una solicitud de ingreso; Kuida completa por WhatsApp los datos que falten.
- EstadoSe envía
patient.upserted- Cuándo se envíaAlta o actualización de un paciente. Kuida lo busca por teléfono, DNI o id externo y, si no existe, lo crea.
- EstadoSe envía
appointment.created- Cuándo se envíaUn turno nuevo. Kuida agenda los recordatorios por WhatsApp que tiene configurados la organización. Sin
externalId, el turno solo se puede cambiar por la API con su id de Kuida. - EstadoSe envía
appointment.rescheduled- Cuándo se envíaCambio de horario de un turno, identificado por su
externalId. Reprograma los recordatorios. - EstadoSe envía
appointment.cancelled- Cuándo se envíaCancelación de un turno, identificado por su
externalId. Da de baja los recordatorios pendientes. - EstadoSe envía
visit.completed- Cuándo se envíaUna consulta atendida. Arranca el seguimiento posconsulta del paciente por WhatsApp.
- EstadoSe envía
treatment.prescribed- Cuándo se envíaUn tratamiento indicado al paciente. Queda registrado en su ficha; Kuida no envía recordatorios de toma de medicación.
- EstadoSe envía
order.issued- Cuándo se envíaUna orden emitida para el paciente: laboratorio, imágenes, interconsulta u otra.
- EstadoTodavía no se envía
intake.created- Cuándo se envíaSe creó una solicitud de ingreso, por cualquiera de sus vías de entrada.
- EstadoSe envía
intake.ready- Cuándo se envíaUna solicitud de ingreso quedó completa y lista para cargar en el sistema de la institución.
- EstadoSe envía
conversation.handoff- Cuándo se envíaUna conversación necesita que intervenga una persona del equipo.
- EstadoTodavía no se envía
patient.silent- Cuándo se envíaUn paciente dejó de responder durante el seguimiento.
- EstadoTodavía no se envía
webhook.ping- Cuándo se envíaEvento de prueba. Lo dispara
POST /v1/webhook_endpoints/{id}/ping. - EstadoSe envía
Los eventos del catálogo de entrada llegan a los webhooks cuando Kuida los procesa, vengan de la API o de cualquier otra vía de entrada. Un tipo marcado como “todavía no se envía” se puede suscribir desde ya y empezará a llegar cuando se active, sin cambios de su lado.
Formato del pedido
Kuida hace un POST con este cuerpo y estos headers:
{
"id": "whd_cm1a2b3c4d5e6f7g8h9i0j",
"object": "event",
"type": "intake.ready",
"apiVersion": "2026-09-28",
"livemode": true,
"occurredAt": "2026-10-05T13:45:00.000Z",
"ref": "cm1a2b3c4d5e6f7g8h9i0j",
"data": {
"patient": {
"fullName": "Paciente Demo",
"dni": "11111111"
},
"requestedService": "Internación domiciliaria",
"intake": {
"id": "cm1a2b3c4d5e6f7g8h9i0j",
"missing": []
}
}
}idstringId de la entrega (
whd_…). Estable entre reintentos: deduplique por este campo.objectstringtypeenumValores:
intake.requestedpatient.upsertedappointment.createdappointment.rescheduledappointment.cancelledvisit.completedtreatment.prescribedorder.issuedintake.createdintake.readyconversation.handoffpatient.silentwebhook.pingapiVersionstringlivemodebooleanoccurredAttimestampCuándo pasó, ISO 8601.
refstringId interno de lo que disparó el evento.
dataobjetoLos datos del hecho.
x-kuida-signature- Contenido
sha256=seguido de la firma en hexadecimal. x-kuida-timestamp- ContenidoMomento del envío, en segundos desde epoch.
x-kuida-event-type- ContenidoEl
typedel evento. x-kuida-delivery-attempt- ContenidoNúmero de intento, desde 1.
user-agent- Contenido
Kuida-Webhooks/1.0
Verificar la firma
Cada pedido está firmado con HMAC-SHA256 usando el secret del endpoint. Verificar la firma garantiza que el pedido viene de Kuida y que el cuerpo no se modificó. Los SDKs lo hacen con Webhook.constructEvent, que verifica la firma y la antigüedad, y devuelve el evento ya interpretado:
import express from "express";
import { Webhook, SignatureVerificationError } from "@kuida/sdk";
const app = express();
// La firma se calcula sobre el cuerpo crudo: no usar express.json() en esta ruta.
app.post("/kuida/webhooks", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = Webhook.constructEvent(
req.body,
req.get("x-kuida-signature"),
req.get("x-kuida-timestamp"),
process.env.KUIDA_WEBHOOK_SECRET,
);
} catch (err) {
if (err instanceof SignatureVerificationError) return res.status(400).send("Firma inválida");
throw err;
}
res.sendStatus(200);
// Procesar después de responder, deduplicando por event.id.
handleEvent(event);
});Usar el cuerpo crudo
La firma se calcula sobre los bytes exactos que envió Kuida. Si el framework interpreta el JSON y se vuelve a serializar, el orden o los espacios cambian y la firma no coincide. Lea el cuerpo como texto o bytes antes de interpretarlo.
Verificar a mano
Sin SDK, la verificación son cuatro pasos:
- Tomar
x-kuida-timestampy rechazar el pedido si difiere en más de 5 minutos de la hora actual. Protege contra el reenvío de pedidos viejos. - Armar el texto firmado: el timestamp, un punto y el cuerpo crudo (
${timestamp}.${cuerpo}). - Calcular HMAC-SHA256 de ese texto con el
secretcomo clave, en hexadecimal, y anteponersha256=. - Comparar con
x-kuida-signatureusando una comparación de tiempo constante.
import crypto from "node:crypto";
export function verifyKuidaSignature(rawBody, signature, timestamp, secret) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Responder y reintentos
Un intento es exitoso si su endpoint responde cualquier 2xx en menos de 10 segundos. Las redirecciones no se siguen: la URL registrada tiene que ser la final. Conviene responder apenas se verifica la firma y procesar el evento después, en una cola.
Si el intento falla, Kuida reintenta hasta completar 9 intentos en 2,7 días:
- 1
- Espera desde el anteriorinmediato
- Desde el primero—
- 2
- Espera desde el anterior1 min
- Desde el primero1 min
- 3
- Espera desde el anterior5 min
- Desde el primero6 min
- 4
- Espera desde el anterior30 min
- Desde el primero36 min
- 5
- Espera desde el anterior2 h
- Desde el primero2 h 36 min
- 6
- Espera desde el anterior5 h
- Desde el primero7 h 36 min
- 7
- Espera desde el anterior10 h
- Desde el primero17 h 36 min
- 8
- Espera desde el anterior1 d
- Desde el primero1 d 17 h
- 9
- Espera desde el anterior1 d
- Desde el primero2 d 17 h
Después del último intento fallido la entrega queda como failed. Pausar o borrar el endpoint corta los reintentos pendientes.
Duplicados y orden
El id del evento (whd_…) es el mismo en todos los reintentos de una entrega. Una red inestable puede hacer que el mismo evento llegue dos veces: guarde los id procesados e ignore los repetidos. El orden de llegada no está garantizado; para ordenar, use occurredAt.
Probar un endpoint
POST /v1/webhook_endpoints/{id}/ping envía un evento webhook.ping firmado, con los mismos reintentos que uno real. Sirve para comprobar la URL y la verificación de firma antes de esperar un evento real.
curl -X POST https://www.kuida.ar/api/v1/webhook_endpoints/we_cm1a2b3c4d5e6f7g8h9i0j/ping \
-H "Authorization: Bearer kd_test_…" \
-H "Idempotency-Key: $(uuidgen)"Revisar entregas
Cada envío queda registrado con sus intentos, el último código de respuesta y el cuerpo enviado. Para ver lo que no llegó:
curl -G https://www.kuida.ar/api/v1/webhook_deliveries \
-H "Authorization: Bearer kd_test_…" \
-d status=failed \
-d limit=10