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.

POST /v1/webhook_endpoints
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"
}'
Respuesta
{
  "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:

Cuerpo
{
  "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": []
    }
  }
}
  • idstring

    Id de la entrega (whd_…). Estable entre reintentos: deduplique por este campo.

  • objectstring
  • typeenum

    Valores:intake.requestedpatient.upsertedappointment.createdappointment.rescheduledappointment.cancelledvisit.completedtreatment.prescribedorder.issuedintake.createdintake.readyconversation.handoffpatient.silentwebhook.ping

  • apiVersionstring
  • livemodeboolean
  • occurredAttimestamp

    Cuándo pasó, ISO 8601.

  • refstring

    Id interno de lo que disparó el evento.

  • dataobjeto

    Los datos del hecho.

x-kuida-signature
Contenidosha256= seguido de la firma en hexadecimal.
x-kuida-timestamp
ContenidoMomento del envío, en segundos desde epoch.
x-kuida-event-type
ContenidoEl type del evento.
x-kuida-delivery-attempt
ContenidoNúmero de intento, desde 1.
user-agent
ContenidoKuida-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:

Verificación con el SDK
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:

  1. Tomar x-kuida-timestamp y rechazar el pedido si difiere en más de 5 minutos de la hora actual. Protege contra el reenvío de pedidos viejos.
  2. Armar el texto firmado: el timestamp, un punto y el cuerpo crudo (${timestamp}.${cuerpo}).
  3. Calcular HMAC-SHA256 de ese texto con el secret como clave, en hexadecimal, y anteponer sha256=.
  4. Comparar con x-kuida-signature usando una comparación de tiempo constante.
Verificación manual
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.

POST /v1/webhook_endpoints/{id}/ping
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ó:

GET /v1/webhook_deliveries
curl -G https://www.kuida.ar/api/v1/webhook_deliveries \
  -H "Authorization: Bearer kd_test_…" \
  -d status=failed \
  -d limit=10