Skip to content
Esto es Web
es

This manual is only available in Spanish for now.

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

  1. 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.
  2. En DocFlow → Integraciones → Destinos, pulsa Añadir destino, pega la URL y marca los eventos que te interesan.
  3. Guarda y pulsa Probar. Debería llegar un webhook.test con 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. inicio es la hora local tal como se guarda, inicio_iso la lleva con desfase y inicio_utc en 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.nombre en 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 3xx cuenta 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

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.