Webhooks
Conectar DocFlow con Kommo, Make, Zapier o n8n para automatizar avisos y crear leads en tu CRM.
- 🛠️ Implantador
DocFlow envía un POST a las URLs que configures cada vez que ocurre algo relevante: una reserva,
una confirmación, una cancelación o un recordatorio antes de la cita. Con eso puedes conectarlo a
Kommo, Make, Zapier, n8n, Pabbly o cualquier servicio que acepte un webhook,
sin escribir una línea de código.
Se configura en DocFlow → Integraciones.
1. Configurar un destino
- Copia la URL que te da tu plataforma:
- Make: módulo Webhooks → Custom webhook → «Copy address to clipboard».
- Zapier: trigger Webhooks by Zapier → Catch Hook → «Your webhook URL».
- n8n: nodo Webhook → URL de producción.
- Kommo: la URL del servicio intermedio que recibe el evento (Make, n8n o tu propio endpoint) y crea el lead vía API de Kommo.
- En DocFlow → Integraciones → Destinos, pulsa Añadir destino, pega la URL y marca los eventos que te interesan.
- Guarda y pulsa Probar. Debería llegar un
webhook.testcon datos de ejemplo.
Consejo para Make y Zapier: pulsa «Probar» mientras la plataforma está a la escucha («Redetermine data structure» en Make, «Test trigger» en Zapier). Así capturan la estructura completa y puedes mapear los campos antes de tener una sola cita real.
Formato
Por defecto el cuerpo es JSON, que es lo que entienden todas las plataformas modernas.
Si tu conector solo acepta application/x-www-form-urlencoded y no maneja estructuras anidadas,
elige el formato Formulario: el mismo contenido se envía aplanado con claves punteadas.
event=cita.creada&data.cita.id=431&data.cita.paciente.telefono_e164=%2B51999888777
Filtros por sede y servicio
(Planes superiores.) Cada destino puede limitarse a determinadas sedes o servicios. Útil si cada sede de la cadena lleva su propio CRM. Sin filtros, el destino recibe la actividad de todas.
Los filtros solo se aplican a los eventos que llevan una cita; los de paciente no se descartan por un filtro de sede, porque una ficha no pertenece a ninguna.
2. Catálogo de eventos
| Clave | Cuándo se dispara |
|---|---|
cita.creada |
Alta de una cita. El campo origen vale publico (reserva del paciente) o gestion. |
cita.actualizada |
Cambia algún dato. Incluye cambios con el antes y el después de cada campo. |
cita.reprogramada |
Cambia la fecha o la hora. Se emite además de cita.actualizada. |
cita.confirmada |
El estado pasa a confirmada. |
cita.cancelada |
El estado pasa a cancelada. |
cita.asignada |
Se asigna cubículo y/o tratante. |
cita.asistencia_registrada |
Se marca asistio, no_asistio, cancelo o reagendo. |
cita.eliminada |
Se borra la cita. |
cita.recordatorio |
Faltan N horas para la cita. horas_antes dice cuál se ha disparado. |
paciente.creado |
Ficha nueva, a mano o creada automáticamente al reservar. |
paciente.actualizado |
Se modifican los datos de una ficha. |
webhook.test |
Envío manual desde el botón «Probar». |
Un mismo guardado puede emitir varios eventos: quien quiera reaccionar a cualquier cambio se
suscribe a cita.actualizada; quien solo quiera actuar sobre las cancelaciones se suscribe a
cita.cancelada y se ahorra inspeccionar el diccionario de cambios en su escenario.
También puedes suscribirte con comodines: cita.*, paciente.* o * (todos, incluidos los que se
añadan en versiones futuras).
3. Estructura del payload
{
"id": "evt_6f3a1c...",
"event": "cita.creada",
"occurred_at": "2026-07-27T14:03:00Z",
"site": {
"url": "https://clinica.example",
"name": "Clínica Ejemplo",
"plugin_version": "0.5.0"
},
"delivery": { "id": 812, "attempt": 1 },
"data": {
"cita": {
"id": 431,
"estado": "pendiente",
"asistencia": "",
"inicio": "2026-07-30 10:00:00",
"inicio_iso": "2026-07-30T10:00:00-05:00",
"inicio_utc": "2026-07-30T15:00:00Z",
"fin": "2026-07-30 10:30:00",
"fin_iso": "2026-07-30T10:30:00-05:00",
"fin_utc": "2026-07-30T15:30:00Z",
"timezone": "America/Lima",
"duracion_min": 30,
"notas": "Primera visita.",
"sede": { "id": 12, "nombre": "Sede Centro" },
"servicio": { "id": 34, "nombre": "Consulta general", "duracion_min": 30, "precio": "80.00", "moneda": "USD" },
"cabina": { "id": 0, "nombre": "" },
"personal": { "id": 21, "nombre": "Dra. Ruiz" },
"paciente": {
"id": 88,
"nombre": "Ana Pérez",
"email": "ana@example.com",
"telefono": "999888777",
"telefono_codigo": "+51",
"telefono_e164": "+51999888777",
"notas": "",
"admin_url": "https://clinica.example/wp-admin/post.php?post=88&action=edit"
},
"creada_en": "2026-07-27 14:03:00",
"admin_url": "https://clinica.example/wp-admin/admin.php?page=docflow-citas&action=edit&cita=431"
},
"origen": "publico"
}
}
Tres detalles pensados para el CRM:
- Fechas en tres formas.
inicioes la hora local tal como se guarda,inicio_isola lleva con desfase yinicio_utcen UTC. Usa la que acepte tu plataforma. - Teléfono en E.164 (
telefono_e164). Internamente el número vive partido en código de país y número; aquí llega ya compuesto, que es lo que exigen WhatsApp, Kommo y las pasarelas de SMS. - Relaciones resueltas.
sede.nombreen vez de un ID que tendrías que buscar aparte.
En los eventos de actualización aparece además cambios:
"cambios": {
"estado": { "antes": "pendiente", "despues": "confirmada" }
}
Puedes ver y copiar el payload de ejemplo de cada evento en la pestaña Eventos.
4. Verificar la firma
Cada envío incluye estas cabeceras:
| Cabecera | Contenido |
|---|---|
X-DocFlow-Event |
Clave del evento. |
X-DocFlow-Delivery |
ID de la entrega. |
X-DocFlow-Attempt |
Número de intento (empieza en 1). |
X-DocFlow-Timestamp |
Marca de tiempo Unix del envío. |
X-DocFlow-Signature |
sha256=<hmac>. |
X-DocFlow-Version |
Versión de DocFlow. |
La firma es el HMAC-SHA256 de timestamp + "." + cuerpo con el secreto del destino (visible al
editarlo). Incluir la marca de tiempo impide que alguien reenvíe una petición capturada.
$base = $_SERVER['HTTP_X_DOCFLOW_TIMESTAMP'] . '.' . file_get_contents( 'php://input' );
$esperada = 'sha256=' . hash_hmac( 'sha256', $base, $secreto );
if ( ! hash_equals( $esperada, $_SERVER['HTTP_X_DOCFLOW_SIGNATURE'] ) ) {
http_response_code( 401 );
exit;
}
// Rechaza lo que tenga más de cinco minutos.
if ( abs( time() - (int) $_SERVER['HTTP_X_DOCFLOW_TIMESTAMP'] ) > 300 ) {
http_response_code( 401 );
exit;
}
const crypto = require('crypto');
const base = `${req.headers['x-docflow-timestamp']}.${rawBody}`;
const esperada = 'sha256=' + crypto.createHmac('sha256', secreto).update(base).digest('hex');
const valida = crypto.timingSafeEqual(
Buffer.from(esperada),
Buffer.from(req.headers['x-docflow-signature'])
);
Verificar la firma es opcional: si tu URL de Make o Zapier ya es secreta, puedes ignorarla.
5. Entrega, reintentos y registro
- No bloquea a nadie. Al ocurrir el evento solo se encola una fila. Que tu CRM esté caído o lento no ralentiza ni impide la reserva del paciente.
- Cuándo sale: en servidores con PHP-FPM, en cuanto se cierra la conexión con el navegador (entrega prácticamente instantánea). En servidores con mod_php, donde eso no es posible, sale por WP-Cron, que se dispara con la siguiente visita al sitio. Si necesitas latencia baja garantizada, configura un cron real (ver §6).
- Éxito = 2xx. Un
3xxcuenta como fallo a propósito: significa que la URL configurada no es la definitiva y conviene que lo veas. - Reintentos: 1 min → 5 min → 30 min → 2 h → 6 h, hasta 5 intentos.
- Endpoints muertos: tras 20 fallos seguidos el destino se desactiva solo, para no machacar indefinidamente una URL caducada. Al reactivarlo el contador vuelve a cero.
- Registro: la pestaña Entregas guarda 30 días de envíos con el cuerpo exacto, el código HTTP, la respuesta del destino y un botón de Reenviar.
Responde con un 200 en cuanto recibas el webhook y procesa después. Si tardas más de 10 segundos,
DocFlow corta y lo cuenta como fallo.
6. Recordatorios de cita
(Planes superiores.) En DocFlow → Integraciones → Recordatorios de cita indicas la antelación
en horas separadas por comas. Con 24,2 cada cita genera un cita.recordatorio un día antes y otro
dos horas antes.
Cada aviso se envía una sola vez por cita y antelación, garantizado por una clave de idempotencia con índice único, aunque el cron se ejecute dos veces o las ventanas se solapen.
Los recordatorios dependen de WP-Cron, que solo se ejecuta cuando alguien visita el sitio. En sitios con poco tráfico conviene desactivarlo y programar un cron real:
// wp-config.php define( 'DISABLE_WP_CRON', true );*/5 * * * * curl -s https://tusitio.com/wp-cron.php?doing_wp_cron > /dev/null
7. Cuotas por plan
| Plan | Destinos | Recordatorios y filtros |
|---|---|---|
| Free | 1 | — |
| Pro | 5 | — |
| Clinic | Sin límite | Sí |
8. Extender desde código
Los eventos se emiten como acciones de WordPress, así que puedes reaccionar sin pasar por HTTP:
// Un evento concreto.
add_action( 'docflow_evento_cita_creada', function ( array $payload ): void {
error_log( $payload['data']['cita']['paciente']['telefono_e164'] );
} );
// Todos los eventos.
add_action( 'docflow_evento', function ( string $evento, array $payload ): void {
// …
}, 10, 2 );
Filtros disponibles:
| Filtro | Para qué |
|---|---|
docflow_webhook_payload |
Adaptar el payload a lo que espera un CRM concreto. Recibe $payload, $evento, $endpoint. |
docflow_webhook_eventos |
Registrar eventos propios en el catálogo. |
docflow_webhook_request_args |
Ajustar la petición saliente (cabeceras, timeout, proxy). |
docflow_webhook_entrega_inmediata |
Devolver false para dejar toda la entrega en manos del cron. |
Para suprimir la emisión durante una operación masiva (importaciones, migraciones):
\DocFlow\Webhooks\Events\Events::suppress( function () {
// Aquí dentro no se emite ningún evento.
} );
9. Resolución de problemas
| Síntoma | Dónde mirar |
|---|---|
| No llega nada | Entregas: si no hay filas, el destino no está suscrito a ese evento o un filtro de sede lo descarta. |
| Llega con retraso | WP-Cron necesita tráfico. Configura un cron real (ver §6). |
| El destino se desactivó solo | 20 fallos seguidos. Mira la respuesta en Entregas, corrige y vuelve a activarlo. |
| Código 0 en el registro | No hubo respuesta: DNS, certificado, firewall o timeout. El mensaje de error lo concreta. |
| Los datos demo no generan eventos | Es intencionado: cargar el dataset de demostración no emite nada. |